incyclist-services 1.7.136 → 1.7.137
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/ride/display/service.js +5 -4
- package/lib/cjs/ride/route/GpxDisplayService.js +3 -3
- package/lib/cjs/ride/route/RLVDisplayService.js +2 -2
- package/lib/cjs/routes/base/parsers/gpx.js +14 -3
- package/lib/cjs/routes/base/parsers/gpxFast.js +203 -0
- package/lib/cjs/routes/base/parsers/incyclist.js +2 -1
- package/lib/cjs/routes/base/parsers/utils.js +15 -10
- package/lib/cjs/routes/base/parsers/xml.js +16 -7
- package/lib/cjs/routes/library/service.js +11 -0
- package/lib/cjs/video/resolvePlaybackUrl.js +1 -3
- package/lib/esm/ride/display/service.js +5 -4
- package/lib/esm/ride/route/GpxDisplayService.js +3 -3
- package/lib/esm/ride/route/RLVDisplayService.js +2 -2
- package/lib/esm/routes/base/parsers/gpx.js +12 -1
- package/lib/esm/routes/base/parsers/gpxFast.js +199 -0
- package/lib/esm/routes/base/parsers/incyclist.js +2 -1
- package/lib/esm/routes/base/parsers/utils.js +15 -10
- package/lib/esm/routes/base/parsers/xml.js +16 -7
- package/lib/esm/routes/library/service.js +11 -0
- package/lib/esm/video/resolvePlaybackUrl.js +1 -3
- package/lib/types/routes/base/parsers/gpx.d.ts +3 -0
- package/lib/types/routes/base/parsers/gpxFast.d.ts +2 -0
- package/lib/types/routes/base/parsers/xml.d.ts +1 -0
- package/lib/types/routes/library/service.d.ts +1 -0
- package/lib/types/routes/library/types.d.ts +2 -0
- package/lib/types/video/resolvePlaybackUrl.d.ts +1 -1
- package/package.json +2 -2
|
@@ -443,10 +443,11 @@ let RideDisplayService = (() => {
|
|
|
443
443
|
}
|
|
444
444
|
async stopRide(props = {}) {
|
|
445
445
|
try {
|
|
446
|
-
if (this.state !== 'Starting' && this.state !== 'Idle') {
|
|
447
|
-
this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: this.state });
|
|
448
|
-
}
|
|
449
446
|
const prevState = this.state;
|
|
447
|
+
const isRedundantCall = prevState === 'Finished' || prevState === 'Idle' || prevState === 'Closing';
|
|
448
|
+
if (!isRedundantCall && this.activity) {
|
|
449
|
+
this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: prevState });
|
|
450
|
+
}
|
|
450
451
|
this.state = prevState === 'Starting' ? 'Closing' : 'Finished';
|
|
451
452
|
this.observer?.stop({ immediately: props.noStateUpdates });
|
|
452
453
|
if (props.noStateUpdates)
|
|
@@ -456,7 +457,7 @@ let RideDisplayService = (() => {
|
|
|
456
457
|
(0, utils_1.waitNextTick)().then(() => { if (this.observer === observerToClear)
|
|
457
458
|
delete this.observer; });
|
|
458
459
|
}
|
|
459
|
-
if (
|
|
460
|
+
if (isRedundantCall) {
|
|
460
461
|
this.state = 'Idle';
|
|
461
462
|
return;
|
|
462
463
|
}
|
|
@@ -140,10 +140,10 @@ let GpxDisplayService = (() => {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
waitsForStreetView() {
|
|
143
|
-
return this.
|
|
143
|
+
return this.getRideSettingsDisplay().getRideView() === 'sv';
|
|
144
144
|
}
|
|
145
145
|
waitsForSatelliteView() {
|
|
146
|
-
return this.
|
|
146
|
+
return this.getRideSettingsDisplay().getRideView() === 'sat';
|
|
147
147
|
}
|
|
148
148
|
getSatelliteViewProps() {
|
|
149
149
|
return {
|
|
@@ -217,8 +217,8 @@ let GpxDisplayService = (() => {
|
|
|
217
217
|
this.clearSatelliteViewStartTimeout();
|
|
218
218
|
}
|
|
219
219
|
else if (state === 'Error') {
|
|
220
|
-
this.logEvent({ message: 'sat view error', error: this.mapError });
|
|
221
220
|
this.mapError = error;
|
|
221
|
+
this.logEvent({ message: 'sat view error', error: this.mapError });
|
|
222
222
|
}
|
|
223
223
|
this.emit('state-update');
|
|
224
224
|
}
|
|
@@ -131,7 +131,7 @@ let RLVDisplayService = (() => {
|
|
|
131
131
|
}
|
|
132
132
|
await this.checkForAdditionalVideos(videoRoute);
|
|
133
133
|
const nextId = (0, routes_1.getNextVideoId)(videoRoute);
|
|
134
|
-
this.logEvent({ message: 'video added', id: videoRoute.description.id, title: videoRoute.description.title, segmentDistance: videoRoute.description.distance, next: nextId });
|
|
134
|
+
this.logEvent({ message: 'video added', id: videoRoute.description.id, title: videoRoute.description.title, segmentDistance: videoRoute.description.distance, next: nextId, src: video?.source });
|
|
135
135
|
}
|
|
136
136
|
catch (err) {
|
|
137
137
|
this.logError(err, 'addVideo');
|
|
@@ -524,7 +524,7 @@ let RLVDisplayService = (() => {
|
|
|
524
524
|
video.source.getObserver().on('convert-error', (message) => this.onConvertError(message, video));
|
|
525
525
|
}
|
|
526
526
|
cleanupUrl(url) {
|
|
527
|
-
return (0, video_1.resolvePlaybackUrl)(url
|
|
527
|
+
return (0, video_1.resolvePlaybackUrl)(url);
|
|
528
528
|
}
|
|
529
529
|
isLoopEnabled() {
|
|
530
530
|
return this.isLoop() && !this.startSettings?.loopOverwrite;
|
|
@@ -4,16 +4,27 @@ exports.GPXParser = void 0;
|
|
|
4
4
|
const utils_1 = require("../../../utils");
|
|
5
5
|
const math_1 = require("../../../utils/math");
|
|
6
6
|
const valid_1 = require("../../../utils/valid");
|
|
7
|
+
const xml_1 = require("../../../utils/xml");
|
|
7
8
|
const route_1 = require("../utils/route");
|
|
8
|
-
const
|
|
9
|
+
const gpxFast_1 = require("./gpxFast");
|
|
10
|
+
const xml_2 = require("./xml");
|
|
9
11
|
const MIN_DISTANCE = 1;
|
|
10
|
-
class GPXParser extends
|
|
12
|
+
class GPXParser extends xml_2.XMLParser {
|
|
11
13
|
static SCHEME = 'gpx';
|
|
12
14
|
props;
|
|
13
15
|
constructor(props = {}) {
|
|
14
16
|
super();
|
|
15
17
|
this.props = props;
|
|
16
18
|
}
|
|
19
|
+
async getData(file, data) {
|
|
20
|
+
if (data)
|
|
21
|
+
return data;
|
|
22
|
+
const resData = await this.readAndDecode(file);
|
|
23
|
+
const fast = (0, gpxFast_1.tryParseGpxRaw)(resData);
|
|
24
|
+
if (fast)
|
|
25
|
+
return new xml_1.XmlJSON(fast, 'gpx');
|
|
26
|
+
return await (0, xml_1.parseXml)(resData);
|
|
27
|
+
}
|
|
17
28
|
getPrimaryExtension() {
|
|
18
29
|
return 'gpx';
|
|
19
30
|
}
|
|
@@ -125,7 +136,7 @@ class GPXParser extends xml_1.XMLParser {
|
|
|
125
136
|
original: gpxPt,
|
|
126
137
|
parsed: point
|
|
127
138
|
};
|
|
128
|
-
this.
|
|
139
|
+
this.getLogger()?.logEvent({ message: 'error', fn: 'loadPoints', reason: 'unvalid point', info });
|
|
129
140
|
return;
|
|
130
141
|
}
|
|
131
142
|
if (this.props.addTime) {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tryParseGpxRaw = void 0;
|
|
4
|
+
const decodeEntities = (s) => s.replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll(''', "'").replaceAll('&', '&');
|
|
5
|
+
const isTagWhitespace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
6
|
+
const isAttrBoundary = (c) => c === '=' || c === '>' || c === '/' || isTagWhitespace(c);
|
|
7
|
+
const skipBoundary = (s, i) => {
|
|
8
|
+
while (i < s.length && (s[i] === '<' || isAttrBoundary(s[i])))
|
|
9
|
+
i++;
|
|
10
|
+
return i;
|
|
11
|
+
};
|
|
12
|
+
const skipWhitespace = (s, i) => {
|
|
13
|
+
while (i < s.length && isTagWhitespace(s[i]))
|
|
14
|
+
i++;
|
|
15
|
+
return i;
|
|
16
|
+
};
|
|
17
|
+
const readToken = (s, i) => {
|
|
18
|
+
const start = i;
|
|
19
|
+
while (i < s.length && !isAttrBoundary(s[i]))
|
|
20
|
+
i++;
|
|
21
|
+
return { token: s.slice(start, i), next: i };
|
|
22
|
+
};
|
|
23
|
+
const readAttr = (s, i) => {
|
|
24
|
+
const { token: name, next: afterName } = readToken(s, i);
|
|
25
|
+
let j = skipWhitespace(s, afterName);
|
|
26
|
+
if (s[j] !== '=')
|
|
27
|
+
return { name, next: j };
|
|
28
|
+
j = skipWhitespace(s, j + 1);
|
|
29
|
+
const quote = s[j];
|
|
30
|
+
if (quote !== '"' && quote !== "'")
|
|
31
|
+
return { name, next: j };
|
|
32
|
+
const valueStart = j + 1;
|
|
33
|
+
const closeIdx = s.indexOf(quote, valueStart);
|
|
34
|
+
if (closeIdx === -1)
|
|
35
|
+
return { name, next: s.length };
|
|
36
|
+
return { name, value: s.slice(valueStart, closeIdx), next: closeIdx + 1 };
|
|
37
|
+
};
|
|
38
|
+
const parseAttrs = (tagText) => {
|
|
39
|
+
const attrs = {};
|
|
40
|
+
let i = 0;
|
|
41
|
+
while (i < tagText.length) {
|
|
42
|
+
i = skipBoundary(tagText, i);
|
|
43
|
+
if (i >= tagText.length)
|
|
44
|
+
break;
|
|
45
|
+
const { name, value, next } = readAttr(tagText, i);
|
|
46
|
+
if (name && value !== undefined)
|
|
47
|
+
attrs[name] = value;
|
|
48
|
+
i = next;
|
|
49
|
+
}
|
|
50
|
+
return attrs;
|
|
51
|
+
};
|
|
52
|
+
const CDATA_RE = /^<!\[CDATA\[([\s\S]*)\]\]>$/;
|
|
53
|
+
const extractLeaf = (block, tag) => {
|
|
54
|
+
const openTag = `<${tag}`;
|
|
55
|
+
const start = block.indexOf(openTag);
|
|
56
|
+
if (start === -1)
|
|
57
|
+
return undefined;
|
|
58
|
+
const afterName = block[start + 1 + tag.length];
|
|
59
|
+
if (afterName !== undefined && !/[\s>/]/.test(afterName))
|
|
60
|
+
return undefined;
|
|
61
|
+
const tagEnd = block.indexOf('>', start);
|
|
62
|
+
if (tagEnd === -1)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (block[tagEnd - 1] === '/')
|
|
65
|
+
return '';
|
|
66
|
+
const closeTag = `</${tag}>`;
|
|
67
|
+
const closeIdx = block.indexOf(closeTag, tagEnd);
|
|
68
|
+
if (closeIdx === -1)
|
|
69
|
+
return undefined;
|
|
70
|
+
const raw = block.slice(tagEnd + 1, closeIdx).trim();
|
|
71
|
+
const cdata = CDATA_RE.exec(raw);
|
|
72
|
+
return cdata ? cdata[1] : decodeEntities(raw);
|
|
73
|
+
};
|
|
74
|
+
const extractBlocks = (text, tag) => {
|
|
75
|
+
const blocks = [];
|
|
76
|
+
let idx = 0;
|
|
77
|
+
while (true) {
|
|
78
|
+
const start = text.indexOf(`<${tag}`, idx);
|
|
79
|
+
if (start === -1)
|
|
80
|
+
break;
|
|
81
|
+
const afterName = text[start + 1 + tag.length];
|
|
82
|
+
if (afterName !== undefined && !/[\s>/]/.test(afterName)) {
|
|
83
|
+
idx = start + 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const tagEnd = text.indexOf('>', start);
|
|
87
|
+
if (tagEnd === -1)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (text[tagEnd - 1] === '/') {
|
|
90
|
+
blocks.push({ body: '' });
|
|
91
|
+
idx = tagEnd + 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const closeTag = `</${tag}>`;
|
|
95
|
+
const closeIdx = text.indexOf(closeTag, tagEnd);
|
|
96
|
+
if (closeIdx === -1)
|
|
97
|
+
return undefined;
|
|
98
|
+
blocks.push({ body: text.slice(tagEnd + 1, closeIdx) });
|
|
99
|
+
idx = closeIdx + closeTag.length;
|
|
100
|
+
}
|
|
101
|
+
return blocks;
|
|
102
|
+
};
|
|
103
|
+
const parseOneTrkpt = (segBody, start) => {
|
|
104
|
+
const tagEnd = segBody.indexOf('>', start);
|
|
105
|
+
if (tagEnd === -1)
|
|
106
|
+
return undefined;
|
|
107
|
+
const attrs = parseAttrs(segBody.slice(start, tagEnd + 1));
|
|
108
|
+
const point = { $: { lat: attrs.lat, lon: attrs.lon } };
|
|
109
|
+
if (segBody[tagEnd - 1] === '/')
|
|
110
|
+
return { point, nextIdx: tagEnd + 1 };
|
|
111
|
+
const closeIdx = segBody.indexOf('</trkpt>', tagEnd);
|
|
112
|
+
if (closeIdx === -1)
|
|
113
|
+
return undefined;
|
|
114
|
+
const body = segBody.slice(tagEnd + 1, closeIdx);
|
|
115
|
+
const ele = extractLeaf(body, 'ele');
|
|
116
|
+
if (ele !== undefined)
|
|
117
|
+
point.ele = [ele];
|
|
118
|
+
const time = extractLeaf(body, 'time');
|
|
119
|
+
if (time !== undefined)
|
|
120
|
+
point.time = [time];
|
|
121
|
+
return { point, nextIdx: closeIdx + '</trkpt>'.length };
|
|
122
|
+
};
|
|
123
|
+
const extractTrkpts = (segBody) => {
|
|
124
|
+
const points = [];
|
|
125
|
+
let idx = 0;
|
|
126
|
+
while (true) {
|
|
127
|
+
const start = segBody.indexOf('<trkpt', idx);
|
|
128
|
+
if (start === -1)
|
|
129
|
+
break;
|
|
130
|
+
const afterName = segBody[start + 6];
|
|
131
|
+
if (afterName !== undefined && !/[\s>/]/.test(afterName)) {
|
|
132
|
+
idx = start + 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const parsed = parseOneTrkpt(segBody, start);
|
|
136
|
+
if (!parsed)
|
|
137
|
+
return undefined;
|
|
138
|
+
points.push(parsed.point);
|
|
139
|
+
idx = parsed.nextIdx;
|
|
140
|
+
}
|
|
141
|
+
return points;
|
|
142
|
+
};
|
|
143
|
+
const buildTrk = (body) => {
|
|
144
|
+
const segStart = body.search(/<trkseg[\s>]/);
|
|
145
|
+
const header = segStart === -1 ? body : body.slice(0, segStart);
|
|
146
|
+
const segBlocks = extractBlocks(body, 'trkseg');
|
|
147
|
+
if (!segBlocks)
|
|
148
|
+
return undefined;
|
|
149
|
+
const trkseg = [];
|
|
150
|
+
for (const seg of segBlocks) {
|
|
151
|
+
const points = extractTrkpts(seg.body);
|
|
152
|
+
if (!points)
|
|
153
|
+
return undefined;
|
|
154
|
+
trkseg.push({ trkpt: points });
|
|
155
|
+
}
|
|
156
|
+
const trkObj = { trkseg };
|
|
157
|
+
const name = extractLeaf(header, 'name');
|
|
158
|
+
if (name !== undefined)
|
|
159
|
+
trkObj.name = [name];
|
|
160
|
+
const desc = extractLeaf(header, 'desc');
|
|
161
|
+
if (desc !== undefined)
|
|
162
|
+
trkObj.desc = [desc];
|
|
163
|
+
return trkObj;
|
|
164
|
+
};
|
|
165
|
+
const buildMetadata = (xmlText) => {
|
|
166
|
+
const metaMatch = /<metadata[\s>][\s\S]*?<\/metadata>/.exec(xmlText);
|
|
167
|
+
if (!metaMatch)
|
|
168
|
+
return undefined;
|
|
169
|
+
const openEnd = metaMatch[0].indexOf('>');
|
|
170
|
+
const closeStart = metaMatch[0].lastIndexOf('</metadata>');
|
|
171
|
+
const body = metaMatch[0].slice(openEnd + 1, closeStart).replace(/<author[\s>][\s\S]*?<\/author>/, '');
|
|
172
|
+
const name = extractLeaf(body, 'name');
|
|
173
|
+
return name !== undefined ? { name: [name] } : undefined;
|
|
174
|
+
};
|
|
175
|
+
const countPoints = (trk) => trk.reduce((sum, t) => sum + t.trkseg.reduce((s, seg) => s + seg.trkpt.length, 0), 0);
|
|
176
|
+
const tryParseGpxRaw = (xmlText) => {
|
|
177
|
+
try {
|
|
178
|
+
if (!/<gpx[\s>]/.test(xmlText))
|
|
179
|
+
return undefined;
|
|
180
|
+
const trkBlocks = extractBlocks(xmlText, 'trk');
|
|
181
|
+
if (!trkBlocks || trkBlocks.length === 0)
|
|
182
|
+
return undefined;
|
|
183
|
+
const trk = [];
|
|
184
|
+
for (const { body } of trkBlocks) {
|
|
185
|
+
const trkObj = buildTrk(body);
|
|
186
|
+
if (!trkObj)
|
|
187
|
+
return undefined;
|
|
188
|
+
trk.push(trkObj);
|
|
189
|
+
}
|
|
190
|
+
const gpx = { trk };
|
|
191
|
+
const metadata = buildMetadata(xmlText);
|
|
192
|
+
if (metadata)
|
|
193
|
+
gpx.metadata = [metadata];
|
|
194
|
+
const literalCount = (xmlText.match(/<trkpt[\s>]/g) ?? []).length;
|
|
195
|
+
if (countPoints(trk) !== literalCount)
|
|
196
|
+
return undefined;
|
|
197
|
+
return { gpx };
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
exports.tryParseGpxRaw = tryParseGpxRaw;
|
|
@@ -49,7 +49,8 @@ class IncyclistXMLParser extends xml_1.XMLParser {
|
|
|
49
49
|
gpxFile.url = gpxFile.url.replace(xmlBase, fileName);
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
|
-
|
|
52
|
+
const encodedFileName = fileInfo.delimiter === '%2F' ? encodeURIComponent(fileName) : fileName;
|
|
53
|
+
gpxFile.filename = `${fileInfo.dir}${fileInfo.delimiter}${encodedFileName}`;
|
|
53
54
|
}
|
|
54
55
|
try {
|
|
55
56
|
let points;
|
|
@@ -142,8 +142,8 @@ const safeEncode = (path) => {
|
|
|
142
142
|
};
|
|
143
143
|
exports.safeEncode = safeEncode;
|
|
144
144
|
const handleFileUrlPath = (fileName) => {
|
|
145
|
-
const cleanPath = (0, exports.safeDecode)(fileName.replace(/^file
|
|
146
|
-
return
|
|
145
|
+
const cleanPath = (0, exports.safeDecode)(fileName.replace(/^file:\/\//, ''));
|
|
146
|
+
return (0, exports.buildFileUrl)(cleanPath);
|
|
147
147
|
};
|
|
148
148
|
const buildFileUrl = (normalizedPath) => {
|
|
149
149
|
if (isAbsolutePath(normalizedPath)) {
|
|
@@ -231,26 +231,31 @@ const decodeUtf16Be = (data) => {
|
|
|
231
231
|
return Buffer.from(swapped.toString('utf16le')).toString('utf-8');
|
|
232
232
|
};
|
|
233
233
|
const getUtf8Data = (res) => {
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
234
|
+
const prefix = Buffer.isBuffer(res) ? res.subarray(0, 4) : Buffer.from(res.slice(0, 4), 'binary');
|
|
235
|
+
if (prefix[0] === 0xFE && prefix[1] === 0xFF) {
|
|
236
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
236
237
|
return decodeUtf16Be(buf.subarray(2));
|
|
237
238
|
}
|
|
238
|
-
if (
|
|
239
|
+
if (prefix[0] === 0xFF && prefix[1] === 0xFE) {
|
|
240
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
239
241
|
return Buffer.from(buf.subarray(2).toString('utf16le')).toString('utf-8');
|
|
240
242
|
}
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
+
if (prefix[0] === 0xFD && prefix[1] === 0xFD) {
|
|
244
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
245
|
+
if (prefix[2] === 0x00 && prefix[3] === 0x3C) {
|
|
243
246
|
return decodeUtf16Be(buf.subarray(2));
|
|
244
247
|
}
|
|
245
|
-
if (
|
|
248
|
+
if (prefix[2] === 0x3C && prefix[3] === 0x00) {
|
|
246
249
|
return Buffer.from(buf.subarray(2).toString('utf16le')).toString('utf-8');
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
|
-
const str = typeof res === 'string' ? res :
|
|
252
|
+
const str = typeof res === 'string' ? res : res.toString('utf-8');
|
|
250
253
|
if (str.codePointAt(0) === 0xFEFF)
|
|
251
254
|
return str.slice(1);
|
|
252
|
-
if (
|
|
255
|
+
if (prefix[0] === 0xEF && prefix[1] === 0xBB && prefix[2] === 0xBF) {
|
|
256
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
253
257
|
return buf.subarray(3).toString('utf-8');
|
|
258
|
+
}
|
|
254
259
|
return str;
|
|
255
260
|
};
|
|
256
261
|
exports.getUtf8Data = getUtf8Data;
|
|
@@ -35,6 +35,10 @@ class XMLParser {
|
|
|
35
35
|
async getData(file, data) {
|
|
36
36
|
if (data)
|
|
37
37
|
return data;
|
|
38
|
+
const resData = await this.readAndDecode(file);
|
|
39
|
+
return await (0, xml_1.parseXml)(resData);
|
|
40
|
+
}
|
|
41
|
+
async readAndDecode(file) {
|
|
38
42
|
const onError = () => {
|
|
39
43
|
throw new Error('Could not open file: ' + (0, utils_2.getFileName)(file));
|
|
40
44
|
};
|
|
@@ -45,9 +49,7 @@ class XMLParser {
|
|
|
45
49
|
this.getLogger().logEvent({ message: '[Parser] getData error', error: res.error });
|
|
46
50
|
onError();
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
const xml = await (0, xml_1.parseXml)(resData);
|
|
50
|
-
return xml;
|
|
52
|
+
return (0, utils_1.getUtf8Data)(res.data);
|
|
51
53
|
}
|
|
52
54
|
catch (err) {
|
|
53
55
|
this.getLogger().logEvent({ message: '[Parser] getData error', error: err.message });
|
|
@@ -278,6 +280,14 @@ class XMLParser {
|
|
|
278
280
|
let prevDistance = 0;
|
|
279
281
|
route.elevation = 0;
|
|
280
282
|
const points = [];
|
|
283
|
+
const altitudeByDistance = new Map();
|
|
284
|
+
if (Array.isArray(altitudes)) {
|
|
285
|
+
altitudes.forEach(a => {
|
|
286
|
+
const d = Number.parseInt(a.distance);
|
|
287
|
+
if (!altitudeByDistance.has(d))
|
|
288
|
+
altitudeByDistance.set(d, a.height);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
281
291
|
positions.forEach((pos, i) => {
|
|
282
292
|
if (i > 0) {
|
|
283
293
|
const prev = positions[i - 1];
|
|
@@ -285,7 +295,7 @@ class XMLParser {
|
|
|
285
295
|
return;
|
|
286
296
|
}
|
|
287
297
|
}
|
|
288
|
-
const altitude = getAltitude(altitudes, positions, i, prevAltitude);
|
|
298
|
+
const altitude = getAltitude(altitudes, altitudeByDistance, positions, i, prevAltitude);
|
|
289
299
|
const elevationGain = altitude - prevAltitude;
|
|
290
300
|
const pi = createPoint(pos, altitude, prevDistance);
|
|
291
301
|
pi.point.cnt = i;
|
|
@@ -342,7 +352,7 @@ function createPoint(pos, altitude, prevDistance) {
|
|
|
342
352
|
}
|
|
343
353
|
return { point, prevDistance };
|
|
344
354
|
}
|
|
345
|
-
function getAltitude(altitudes, positions, i, prevAltitude) {
|
|
355
|
+
function getAltitude(altitudes, altitudeByDistance, positions, i, prevAltitude) {
|
|
346
356
|
const pos = positions[i];
|
|
347
357
|
let height = altitudes[i]?.height;
|
|
348
358
|
if (altitudes && Array.isArray(altitudes)) {
|
|
@@ -350,8 +360,7 @@ function getAltitude(altitudes, positions, i, prevAltitude) {
|
|
|
350
360
|
return height !== undefined ? Number(height) : prevAltitude;
|
|
351
361
|
}
|
|
352
362
|
else {
|
|
353
|
-
|
|
354
|
-
height = altFound?.height;
|
|
363
|
+
height = altitudeByDistance.get(Number.parseInt(pos.distance));
|
|
355
364
|
return height !== undefined ? Number(height) : prevAltitude;
|
|
356
365
|
}
|
|
357
366
|
}
|
|
@@ -371,6 +371,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
371
371
|
const file = this.buildFileInfo(target.controlFileUri, target.format);
|
|
372
372
|
this.importProps.routes.push({
|
|
373
373
|
format: target.format,
|
|
374
|
+
parseState: 'waiting',
|
|
374
375
|
importable: false,
|
|
375
376
|
label: file.base,
|
|
376
377
|
id: target.controlFileUri,
|
|
@@ -401,8 +402,12 @@ let RouteLibraryScannerService = (() => {
|
|
|
401
402
|
async _parseTarget(target, service, observer) {
|
|
402
403
|
let result;
|
|
403
404
|
const file = this.buildFileInfo(target.controlFileUri, target.format);
|
|
405
|
+
const importProps = this.importProps.routes.find(r => r.id === target.controlFileUri) ?? {};
|
|
404
406
|
try {
|
|
407
|
+
importProps.parseState = 'parsing';
|
|
408
|
+
observer.emit('updated', importProps);
|
|
405
409
|
result = await parsers_1.RouteParser.parse(file);
|
|
410
|
+
importProps.parseState = 'parsed';
|
|
406
411
|
const route = new route_1.Route(result.data, result.details);
|
|
407
412
|
const existing = this.importProps.routes.find(ri => ri.id === route.description.id);
|
|
408
413
|
if (existing) {
|
|
@@ -425,6 +430,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
425
430
|
observer.emit('parse-result', parsed);
|
|
426
431
|
}
|
|
427
432
|
catch (err) {
|
|
433
|
+
importProps.parseState = 'parsed';
|
|
428
434
|
const parsed = {
|
|
429
435
|
alreadyImported: false,
|
|
430
436
|
route: result ? new route_1.Route(result.data, result.details) : undefined,
|
|
@@ -581,6 +587,9 @@ let RouteLibraryScannerService = (() => {
|
|
|
581
587
|
delimiter: uri?.startsWith('content://') ? '%2F' : '/'
|
|
582
588
|
};
|
|
583
589
|
}
|
|
590
|
+
getImportProps(parsed) {
|
|
591
|
+
return this.importProps.routes.find(r => r.id === parsed.controlFileUri);
|
|
592
|
+
}
|
|
584
593
|
buildRouteDisplayItem(parsed, observer) {
|
|
585
594
|
const { route, alreadyImported, parseError, format, controlFileUri } = parsed;
|
|
586
595
|
const descr = route?.description ?? {};
|
|
@@ -591,11 +600,13 @@ let RouteLibraryScannerService = (() => {
|
|
|
591
600
|
};
|
|
592
601
|
const path = this.getBindings().path;
|
|
593
602
|
const info = path.parse(controlFileUri);
|
|
603
|
+
const importProps = this.getImportProps(parsed);
|
|
594
604
|
return {
|
|
595
605
|
id: route?.description?.id ?? info?.base,
|
|
596
606
|
distance,
|
|
597
607
|
label: route?.title ?? info?.base,
|
|
598
608
|
alreadyImported,
|
|
609
|
+
parseState: importProps?.parseState ?? 'waiting',
|
|
599
610
|
importable: parseError == null,
|
|
600
611
|
format,
|
|
601
612
|
errorReason: parseError,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolvePlaybackUrl = void 0;
|
|
4
|
-
const resolvePlaybackUrl = (url
|
|
4
|
+
const resolvePlaybackUrl = (url) => {
|
|
5
5
|
if (!url)
|
|
6
6
|
return undefined;
|
|
7
7
|
let fileName = url;
|
|
@@ -10,8 +10,6 @@ const resolvePlaybackUrl = (url, isMobile = false) => {
|
|
|
10
10
|
fileName = fileName.replace('incyclist:', 'file:');
|
|
11
11
|
if (fileName.startsWith('video:') && !lc.endsWith('.avi'))
|
|
12
12
|
return fileName.replace('video:', 'file:');
|
|
13
|
-
if (fileName.startsWith('file:') && !lc.endsWith('.avi') && !isMobile)
|
|
14
|
-
return fileName.replace('file:', 'video:');
|
|
15
13
|
if (fileName.startsWith('video:') && lc.endsWith('.avi'))
|
|
16
14
|
return fileName;
|
|
17
15
|
if (fileName.startsWith('file:') || fileName.startsWith('http:') || fileName.startsWith('https:') || fileName.startsWith('/'))
|
|
@@ -440,10 +440,11 @@ let RideDisplayService = (() => {
|
|
|
440
440
|
}
|
|
441
441
|
async stopRide(props = {}) {
|
|
442
442
|
try {
|
|
443
|
-
if (this.state !== 'Starting' && this.state !== 'Idle') {
|
|
444
|
-
this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: this.state });
|
|
445
|
-
}
|
|
446
443
|
const prevState = this.state;
|
|
444
|
+
const isRedundantCall = prevState === 'Finished' || prevState === 'Idle' || prevState === 'Closing';
|
|
445
|
+
if (!isRedundantCall && this.activity) {
|
|
446
|
+
this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: prevState });
|
|
447
|
+
}
|
|
447
448
|
this.state = prevState === 'Starting' ? 'Closing' : 'Finished';
|
|
448
449
|
this.observer?.stop({ immediately: props.noStateUpdates });
|
|
449
450
|
if (props.noStateUpdates)
|
|
@@ -453,7 +454,7 @@ let RideDisplayService = (() => {
|
|
|
453
454
|
waitNextTick().then(() => { if (this.observer === observerToClear)
|
|
454
455
|
delete this.observer; });
|
|
455
456
|
}
|
|
456
|
-
if (
|
|
457
|
+
if (isRedundantCall) {
|
|
457
458
|
this.state = 'Idle';
|
|
458
459
|
return;
|
|
459
460
|
}
|
|
@@ -137,10 +137,10 @@ let GpxDisplayService = (() => {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
waitsForStreetView() {
|
|
140
|
-
return this.
|
|
140
|
+
return this.getRideSettingsDisplay().getRideView() === 'sv';
|
|
141
141
|
}
|
|
142
142
|
waitsForSatelliteView() {
|
|
143
|
-
return this.
|
|
143
|
+
return this.getRideSettingsDisplay().getRideView() === 'sat';
|
|
144
144
|
}
|
|
145
145
|
getSatelliteViewProps() {
|
|
146
146
|
return {
|
|
@@ -214,8 +214,8 @@ let GpxDisplayService = (() => {
|
|
|
214
214
|
this.clearSatelliteViewStartTimeout();
|
|
215
215
|
}
|
|
216
216
|
else if (state === 'Error') {
|
|
217
|
-
this.logEvent({ message: 'sat view error', error: this.mapError });
|
|
218
217
|
this.mapError = error;
|
|
218
|
+
this.logEvent({ message: 'sat view error', error: this.mapError });
|
|
219
219
|
}
|
|
220
220
|
this.emit('state-update');
|
|
221
221
|
}
|
|
@@ -128,7 +128,7 @@ let RLVDisplayService = (() => {
|
|
|
128
128
|
}
|
|
129
129
|
await this.checkForAdditionalVideos(videoRoute);
|
|
130
130
|
const nextId = getNextVideoId(videoRoute);
|
|
131
|
-
this.logEvent({ message: 'video added', id: videoRoute.description.id, title: videoRoute.description.title, segmentDistance: videoRoute.description.distance, next: nextId });
|
|
131
|
+
this.logEvent({ message: 'video added', id: videoRoute.description.id, title: videoRoute.description.title, segmentDistance: videoRoute.description.distance, next: nextId, src: video?.source });
|
|
132
132
|
}
|
|
133
133
|
catch (err) {
|
|
134
134
|
this.logError(err, 'addVideo');
|
|
@@ -521,7 +521,7 @@ let RLVDisplayService = (() => {
|
|
|
521
521
|
video.source.getObserver().on('convert-error', (message) => this.onConvertError(message, video));
|
|
522
522
|
}
|
|
523
523
|
cleanupUrl(url) {
|
|
524
|
-
return resolvePlaybackUrl(url
|
|
524
|
+
return resolvePlaybackUrl(url);
|
|
525
525
|
}
|
|
526
526
|
isLoopEnabled() {
|
|
527
527
|
return this.isLoop() && !this.startSettings?.loopOverwrite;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { geo } from "../../../utils";
|
|
2
2
|
import { num } from "../../../utils/math";
|
|
3
3
|
import { valid } from "../../../utils/valid";
|
|
4
|
+
import { XmlJSON, parseXml } from "../../../utils/xml";
|
|
4
5
|
import { checkIsLoop, getRouteHash } from "../utils/route";
|
|
6
|
+
import { tryParseGpxRaw } from "./gpxFast";
|
|
5
7
|
import { XMLParser } from "./xml";
|
|
6
8
|
const MIN_DISTANCE = 1;
|
|
7
9
|
export class GPXParser extends XMLParser {
|
|
@@ -11,6 +13,15 @@ export class GPXParser extends XMLParser {
|
|
|
11
13
|
super();
|
|
12
14
|
this.props = props;
|
|
13
15
|
}
|
|
16
|
+
async getData(file, data) {
|
|
17
|
+
if (data)
|
|
18
|
+
return data;
|
|
19
|
+
const resData = await this.readAndDecode(file);
|
|
20
|
+
const fast = tryParseGpxRaw(resData);
|
|
21
|
+
if (fast)
|
|
22
|
+
return new XmlJSON(fast, 'gpx');
|
|
23
|
+
return await parseXml(resData);
|
|
24
|
+
}
|
|
14
25
|
getPrimaryExtension() {
|
|
15
26
|
return 'gpx';
|
|
16
27
|
}
|
|
@@ -122,7 +133,7 @@ export class GPXParser extends XMLParser {
|
|
|
122
133
|
original: gpxPt,
|
|
123
134
|
parsed: point
|
|
124
135
|
};
|
|
125
|
-
this.
|
|
136
|
+
this.getLogger()?.logEvent({ message: 'error', fn: 'loadPoints', reason: 'unvalid point', info });
|
|
126
137
|
return;
|
|
127
138
|
}
|
|
128
139
|
if (this.props.addTime) {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const decodeEntities = (s) => s.replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll(''', "'").replaceAll('&', '&');
|
|
2
|
+
const isTagWhitespace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
3
|
+
const isAttrBoundary = (c) => c === '=' || c === '>' || c === '/' || isTagWhitespace(c);
|
|
4
|
+
const skipBoundary = (s, i) => {
|
|
5
|
+
while (i < s.length && (s[i] === '<' || isAttrBoundary(s[i])))
|
|
6
|
+
i++;
|
|
7
|
+
return i;
|
|
8
|
+
};
|
|
9
|
+
const skipWhitespace = (s, i) => {
|
|
10
|
+
while (i < s.length && isTagWhitespace(s[i]))
|
|
11
|
+
i++;
|
|
12
|
+
return i;
|
|
13
|
+
};
|
|
14
|
+
const readToken = (s, i) => {
|
|
15
|
+
const start = i;
|
|
16
|
+
while (i < s.length && !isAttrBoundary(s[i]))
|
|
17
|
+
i++;
|
|
18
|
+
return { token: s.slice(start, i), next: i };
|
|
19
|
+
};
|
|
20
|
+
const readAttr = (s, i) => {
|
|
21
|
+
const { token: name, next: afterName } = readToken(s, i);
|
|
22
|
+
let j = skipWhitespace(s, afterName);
|
|
23
|
+
if (s[j] !== '=')
|
|
24
|
+
return { name, next: j };
|
|
25
|
+
j = skipWhitespace(s, j + 1);
|
|
26
|
+
const quote = s[j];
|
|
27
|
+
if (quote !== '"' && quote !== "'")
|
|
28
|
+
return { name, next: j };
|
|
29
|
+
const valueStart = j + 1;
|
|
30
|
+
const closeIdx = s.indexOf(quote, valueStart);
|
|
31
|
+
if (closeIdx === -1)
|
|
32
|
+
return { name, next: s.length };
|
|
33
|
+
return { name, value: s.slice(valueStart, closeIdx), next: closeIdx + 1 };
|
|
34
|
+
};
|
|
35
|
+
const parseAttrs = (tagText) => {
|
|
36
|
+
const attrs = {};
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < tagText.length) {
|
|
39
|
+
i = skipBoundary(tagText, i);
|
|
40
|
+
if (i >= tagText.length)
|
|
41
|
+
break;
|
|
42
|
+
const { name, value, next } = readAttr(tagText, i);
|
|
43
|
+
if (name && value !== undefined)
|
|
44
|
+
attrs[name] = value;
|
|
45
|
+
i = next;
|
|
46
|
+
}
|
|
47
|
+
return attrs;
|
|
48
|
+
};
|
|
49
|
+
const CDATA_RE = /^<!\[CDATA\[([\s\S]*)\]\]>$/;
|
|
50
|
+
const extractLeaf = (block, tag) => {
|
|
51
|
+
const openTag = `<${tag}`;
|
|
52
|
+
const start = block.indexOf(openTag);
|
|
53
|
+
if (start === -1)
|
|
54
|
+
return undefined;
|
|
55
|
+
const afterName = block[start + 1 + tag.length];
|
|
56
|
+
if (afterName !== undefined && !/[\s>/]/.test(afterName))
|
|
57
|
+
return undefined;
|
|
58
|
+
const tagEnd = block.indexOf('>', start);
|
|
59
|
+
if (tagEnd === -1)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (block[tagEnd - 1] === '/')
|
|
62
|
+
return '';
|
|
63
|
+
const closeTag = `</${tag}>`;
|
|
64
|
+
const closeIdx = block.indexOf(closeTag, tagEnd);
|
|
65
|
+
if (closeIdx === -1)
|
|
66
|
+
return undefined;
|
|
67
|
+
const raw = block.slice(tagEnd + 1, closeIdx).trim();
|
|
68
|
+
const cdata = CDATA_RE.exec(raw);
|
|
69
|
+
return cdata ? cdata[1] : decodeEntities(raw);
|
|
70
|
+
};
|
|
71
|
+
const extractBlocks = (text, tag) => {
|
|
72
|
+
const blocks = [];
|
|
73
|
+
let idx = 0;
|
|
74
|
+
while (true) {
|
|
75
|
+
const start = text.indexOf(`<${tag}`, idx);
|
|
76
|
+
if (start === -1)
|
|
77
|
+
break;
|
|
78
|
+
const afterName = text[start + 1 + tag.length];
|
|
79
|
+
if (afterName !== undefined && !/[\s>/]/.test(afterName)) {
|
|
80
|
+
idx = start + 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const tagEnd = text.indexOf('>', start);
|
|
84
|
+
if (tagEnd === -1)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (text[tagEnd - 1] === '/') {
|
|
87
|
+
blocks.push({ body: '' });
|
|
88
|
+
idx = tagEnd + 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const closeTag = `</${tag}>`;
|
|
92
|
+
const closeIdx = text.indexOf(closeTag, tagEnd);
|
|
93
|
+
if (closeIdx === -1)
|
|
94
|
+
return undefined;
|
|
95
|
+
blocks.push({ body: text.slice(tagEnd + 1, closeIdx) });
|
|
96
|
+
idx = closeIdx + closeTag.length;
|
|
97
|
+
}
|
|
98
|
+
return blocks;
|
|
99
|
+
};
|
|
100
|
+
const parseOneTrkpt = (segBody, start) => {
|
|
101
|
+
const tagEnd = segBody.indexOf('>', start);
|
|
102
|
+
if (tagEnd === -1)
|
|
103
|
+
return undefined;
|
|
104
|
+
const attrs = parseAttrs(segBody.slice(start, tagEnd + 1));
|
|
105
|
+
const point = { $: { lat: attrs.lat, lon: attrs.lon } };
|
|
106
|
+
if (segBody[tagEnd - 1] === '/')
|
|
107
|
+
return { point, nextIdx: tagEnd + 1 };
|
|
108
|
+
const closeIdx = segBody.indexOf('</trkpt>', tagEnd);
|
|
109
|
+
if (closeIdx === -1)
|
|
110
|
+
return undefined;
|
|
111
|
+
const body = segBody.slice(tagEnd + 1, closeIdx);
|
|
112
|
+
const ele = extractLeaf(body, 'ele');
|
|
113
|
+
if (ele !== undefined)
|
|
114
|
+
point.ele = [ele];
|
|
115
|
+
const time = extractLeaf(body, 'time');
|
|
116
|
+
if (time !== undefined)
|
|
117
|
+
point.time = [time];
|
|
118
|
+
return { point, nextIdx: closeIdx + '</trkpt>'.length };
|
|
119
|
+
};
|
|
120
|
+
const extractTrkpts = (segBody) => {
|
|
121
|
+
const points = [];
|
|
122
|
+
let idx = 0;
|
|
123
|
+
while (true) {
|
|
124
|
+
const start = segBody.indexOf('<trkpt', idx);
|
|
125
|
+
if (start === -1)
|
|
126
|
+
break;
|
|
127
|
+
const afterName = segBody[start + 6];
|
|
128
|
+
if (afterName !== undefined && !/[\s>/]/.test(afterName)) {
|
|
129
|
+
idx = start + 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const parsed = parseOneTrkpt(segBody, start);
|
|
133
|
+
if (!parsed)
|
|
134
|
+
return undefined;
|
|
135
|
+
points.push(parsed.point);
|
|
136
|
+
idx = parsed.nextIdx;
|
|
137
|
+
}
|
|
138
|
+
return points;
|
|
139
|
+
};
|
|
140
|
+
const buildTrk = (body) => {
|
|
141
|
+
const segStart = body.search(/<trkseg[\s>]/);
|
|
142
|
+
const header = segStart === -1 ? body : body.slice(0, segStart);
|
|
143
|
+
const segBlocks = extractBlocks(body, 'trkseg');
|
|
144
|
+
if (!segBlocks)
|
|
145
|
+
return undefined;
|
|
146
|
+
const trkseg = [];
|
|
147
|
+
for (const seg of segBlocks) {
|
|
148
|
+
const points = extractTrkpts(seg.body);
|
|
149
|
+
if (!points)
|
|
150
|
+
return undefined;
|
|
151
|
+
trkseg.push({ trkpt: points });
|
|
152
|
+
}
|
|
153
|
+
const trkObj = { trkseg };
|
|
154
|
+
const name = extractLeaf(header, 'name');
|
|
155
|
+
if (name !== undefined)
|
|
156
|
+
trkObj.name = [name];
|
|
157
|
+
const desc = extractLeaf(header, 'desc');
|
|
158
|
+
if (desc !== undefined)
|
|
159
|
+
trkObj.desc = [desc];
|
|
160
|
+
return trkObj;
|
|
161
|
+
};
|
|
162
|
+
const buildMetadata = (xmlText) => {
|
|
163
|
+
const metaMatch = /<metadata[\s>][\s\S]*?<\/metadata>/.exec(xmlText);
|
|
164
|
+
if (!metaMatch)
|
|
165
|
+
return undefined;
|
|
166
|
+
const openEnd = metaMatch[0].indexOf('>');
|
|
167
|
+
const closeStart = metaMatch[0].lastIndexOf('</metadata>');
|
|
168
|
+
const body = metaMatch[0].slice(openEnd + 1, closeStart).replace(/<author[\s>][\s\S]*?<\/author>/, '');
|
|
169
|
+
const name = extractLeaf(body, 'name');
|
|
170
|
+
return name !== undefined ? { name: [name] } : undefined;
|
|
171
|
+
};
|
|
172
|
+
const countPoints = (trk) => trk.reduce((sum, t) => sum + t.trkseg.reduce((s, seg) => s + seg.trkpt.length, 0), 0);
|
|
173
|
+
export const tryParseGpxRaw = (xmlText) => {
|
|
174
|
+
try {
|
|
175
|
+
if (!/<gpx[\s>]/.test(xmlText))
|
|
176
|
+
return undefined;
|
|
177
|
+
const trkBlocks = extractBlocks(xmlText, 'trk');
|
|
178
|
+
if (!trkBlocks || trkBlocks.length === 0)
|
|
179
|
+
return undefined;
|
|
180
|
+
const trk = [];
|
|
181
|
+
for (const { body } of trkBlocks) {
|
|
182
|
+
const trkObj = buildTrk(body);
|
|
183
|
+
if (!trkObj)
|
|
184
|
+
return undefined;
|
|
185
|
+
trk.push(trkObj);
|
|
186
|
+
}
|
|
187
|
+
const gpx = { trk };
|
|
188
|
+
const metadata = buildMetadata(xmlText);
|
|
189
|
+
if (metadata)
|
|
190
|
+
gpx.metadata = [metadata];
|
|
191
|
+
const literalCount = (xmlText.match(/<trkpt[\s>]/g) ?? []).length;
|
|
192
|
+
if (countPoints(trk) !== literalCount)
|
|
193
|
+
return undefined;
|
|
194
|
+
return { gpx };
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
@@ -46,7 +46,8 @@ export class IncyclistXMLParser extends XMLParser {
|
|
|
46
46
|
gpxFile.url = gpxFile.url.replace(xmlBase, fileName);
|
|
47
47
|
}
|
|
48
48
|
else {
|
|
49
|
-
|
|
49
|
+
const encodedFileName = fileInfo.delimiter === '%2F' ? encodeURIComponent(fileName) : fileName;
|
|
50
|
+
gpxFile.filename = `${fileInfo.dir}${fileInfo.delimiter}${encodedFileName}`;
|
|
50
51
|
}
|
|
51
52
|
try {
|
|
52
53
|
let points;
|
|
@@ -134,8 +134,8 @@ export const safeEncode = (path) => {
|
|
|
134
134
|
return encoded;
|
|
135
135
|
};
|
|
136
136
|
const handleFileUrlPath = (fileName) => {
|
|
137
|
-
const cleanPath = safeDecode(fileName.replace(/^file
|
|
138
|
-
return
|
|
137
|
+
const cleanPath = safeDecode(fileName.replace(/^file:\/\//, ''));
|
|
138
|
+
return buildFileUrl(cleanPath);
|
|
139
139
|
};
|
|
140
140
|
export const buildFileUrl = (normalizedPath) => {
|
|
141
141
|
if (isAbsolutePath(normalizedPath)) {
|
|
@@ -220,25 +220,30 @@ const decodeUtf16Be = (data) => {
|
|
|
220
220
|
return Buffer.from(swapped.toString('utf16le')).toString('utf-8');
|
|
221
221
|
};
|
|
222
222
|
export const getUtf8Data = (res) => {
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
223
|
+
const prefix = Buffer.isBuffer(res) ? res.subarray(0, 4) : Buffer.from(res.slice(0, 4), 'binary');
|
|
224
|
+
if (prefix[0] === 0xFE && prefix[1] === 0xFF) {
|
|
225
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
225
226
|
return decodeUtf16Be(buf.subarray(2));
|
|
226
227
|
}
|
|
227
|
-
if (
|
|
228
|
+
if (prefix[0] === 0xFF && prefix[1] === 0xFE) {
|
|
229
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
228
230
|
return Buffer.from(buf.subarray(2).toString('utf16le')).toString('utf-8');
|
|
229
231
|
}
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
+
if (prefix[0] === 0xFD && prefix[1] === 0xFD) {
|
|
233
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
234
|
+
if (prefix[2] === 0x00 && prefix[3] === 0x3C) {
|
|
232
235
|
return decodeUtf16Be(buf.subarray(2));
|
|
233
236
|
}
|
|
234
|
-
if (
|
|
237
|
+
if (prefix[2] === 0x3C && prefix[3] === 0x00) {
|
|
235
238
|
return Buffer.from(buf.subarray(2).toString('utf16le')).toString('utf-8');
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
|
-
const str = typeof res === 'string' ? res :
|
|
241
|
+
const str = typeof res === 'string' ? res : res.toString('utf-8');
|
|
239
242
|
if (str.codePointAt(0) === 0xFEFF)
|
|
240
243
|
return str.slice(1);
|
|
241
|
-
if (
|
|
244
|
+
if (prefix[0] === 0xEF && prefix[1] === 0xBB && prefix[2] === 0xBF) {
|
|
245
|
+
const buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
|
|
242
246
|
return buf.subarray(3).toString('utf-8');
|
|
247
|
+
}
|
|
243
248
|
return str;
|
|
244
249
|
};
|
|
@@ -32,6 +32,10 @@ export class XMLParser {
|
|
|
32
32
|
async getData(file, data) {
|
|
33
33
|
if (data)
|
|
34
34
|
return data;
|
|
35
|
+
const resData = await this.readAndDecode(file);
|
|
36
|
+
return await parseXml(resData);
|
|
37
|
+
}
|
|
38
|
+
async readAndDecode(file) {
|
|
35
39
|
const onError = () => {
|
|
36
40
|
throw new Error('Could not open file: ' + getFileName(file));
|
|
37
41
|
};
|
|
@@ -42,9 +46,7 @@ export class XMLParser {
|
|
|
42
46
|
this.getLogger().logEvent({ message: '[Parser] getData error', error: res.error });
|
|
43
47
|
onError();
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
const xml = await parseXml(resData);
|
|
47
|
-
return xml;
|
|
49
|
+
return getUtf8Data(res.data);
|
|
48
50
|
}
|
|
49
51
|
catch (err) {
|
|
50
52
|
this.getLogger().logEvent({ message: '[Parser] getData error', error: err.message });
|
|
@@ -275,6 +277,14 @@ export class XMLParser {
|
|
|
275
277
|
let prevDistance = 0;
|
|
276
278
|
route.elevation = 0;
|
|
277
279
|
const points = [];
|
|
280
|
+
const altitudeByDistance = new Map();
|
|
281
|
+
if (Array.isArray(altitudes)) {
|
|
282
|
+
altitudes.forEach(a => {
|
|
283
|
+
const d = Number.parseInt(a.distance);
|
|
284
|
+
if (!altitudeByDistance.has(d))
|
|
285
|
+
altitudeByDistance.set(d, a.height);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
278
288
|
positions.forEach((pos, i) => {
|
|
279
289
|
if (i > 0) {
|
|
280
290
|
const prev = positions[i - 1];
|
|
@@ -282,7 +292,7 @@ export class XMLParser {
|
|
|
282
292
|
return;
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
|
-
const altitude = getAltitude(altitudes, positions, i, prevAltitude);
|
|
295
|
+
const altitude = getAltitude(altitudes, altitudeByDistance, positions, i, prevAltitude);
|
|
286
296
|
const elevationGain = altitude - prevAltitude;
|
|
287
297
|
const pi = createPoint(pos, altitude, prevDistance);
|
|
288
298
|
pi.point.cnt = i;
|
|
@@ -338,7 +348,7 @@ function createPoint(pos, altitude, prevDistance) {
|
|
|
338
348
|
}
|
|
339
349
|
return { point, prevDistance };
|
|
340
350
|
}
|
|
341
|
-
function getAltitude(altitudes, positions, i, prevAltitude) {
|
|
351
|
+
function getAltitude(altitudes, altitudeByDistance, positions, i, prevAltitude) {
|
|
342
352
|
const pos = positions[i];
|
|
343
353
|
let height = altitudes[i]?.height;
|
|
344
354
|
if (altitudes && Array.isArray(altitudes)) {
|
|
@@ -346,8 +356,7 @@ function getAltitude(altitudes, positions, i, prevAltitude) {
|
|
|
346
356
|
return height !== undefined ? Number(height) : prevAltitude;
|
|
347
357
|
}
|
|
348
358
|
else {
|
|
349
|
-
|
|
350
|
-
height = altFound?.height;
|
|
359
|
+
height = altitudeByDistance.get(Number.parseInt(pos.distance));
|
|
351
360
|
return height !== undefined ? Number(height) : prevAltitude;
|
|
352
361
|
}
|
|
353
362
|
}
|
|
@@ -368,6 +368,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
368
368
|
const file = this.buildFileInfo(target.controlFileUri, target.format);
|
|
369
369
|
this.importProps.routes.push({
|
|
370
370
|
format: target.format,
|
|
371
|
+
parseState: 'waiting',
|
|
371
372
|
importable: false,
|
|
372
373
|
label: file.base,
|
|
373
374
|
id: target.controlFileUri,
|
|
@@ -398,8 +399,12 @@ let RouteLibraryScannerService = (() => {
|
|
|
398
399
|
async _parseTarget(target, service, observer) {
|
|
399
400
|
let result;
|
|
400
401
|
const file = this.buildFileInfo(target.controlFileUri, target.format);
|
|
402
|
+
const importProps = this.importProps.routes.find(r => r.id === target.controlFileUri) ?? {};
|
|
401
403
|
try {
|
|
404
|
+
importProps.parseState = 'parsing';
|
|
405
|
+
observer.emit('updated', importProps);
|
|
402
406
|
result = await RouteParser.parse(file);
|
|
407
|
+
importProps.parseState = 'parsed';
|
|
403
408
|
const route = new Route(result.data, result.details);
|
|
404
409
|
const existing = this.importProps.routes.find(ri => ri.id === route.description.id);
|
|
405
410
|
if (existing) {
|
|
@@ -422,6 +427,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
422
427
|
observer.emit('parse-result', parsed);
|
|
423
428
|
}
|
|
424
429
|
catch (err) {
|
|
430
|
+
importProps.parseState = 'parsed';
|
|
425
431
|
const parsed = {
|
|
426
432
|
alreadyImported: false,
|
|
427
433
|
route: result ? new Route(result.data, result.details) : undefined,
|
|
@@ -578,6 +584,9 @@ let RouteLibraryScannerService = (() => {
|
|
|
578
584
|
delimiter: uri?.startsWith('content://') ? '%2F' : '/'
|
|
579
585
|
};
|
|
580
586
|
}
|
|
587
|
+
getImportProps(parsed) {
|
|
588
|
+
return this.importProps.routes.find(r => r.id === parsed.controlFileUri);
|
|
589
|
+
}
|
|
581
590
|
buildRouteDisplayItem(parsed, observer) {
|
|
582
591
|
const { route, alreadyImported, parseError, format, controlFileUri } = parsed;
|
|
583
592
|
const descr = route?.description ?? {};
|
|
@@ -588,11 +597,13 @@ let RouteLibraryScannerService = (() => {
|
|
|
588
597
|
};
|
|
589
598
|
const path = this.getBindings().path;
|
|
590
599
|
const info = path.parse(controlFileUri);
|
|
600
|
+
const importProps = this.getImportProps(parsed);
|
|
591
601
|
return {
|
|
592
602
|
id: route?.description?.id ?? info?.base,
|
|
593
603
|
distance,
|
|
594
604
|
label: route?.title ?? info?.base,
|
|
595
605
|
alreadyImported,
|
|
606
|
+
parseState: importProps?.parseState ?? 'waiting',
|
|
596
607
|
importable: parseError == null,
|
|
597
608
|
format,
|
|
598
609
|
errorReason: parseError,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const resolvePlaybackUrl = (url
|
|
1
|
+
export const resolvePlaybackUrl = (url) => {
|
|
2
2
|
if (!url)
|
|
3
3
|
return undefined;
|
|
4
4
|
let fileName = url;
|
|
@@ -7,8 +7,6 @@ export const resolvePlaybackUrl = (url, isMobile = false) => {
|
|
|
7
7
|
fileName = fileName.replace('incyclist:', 'file:');
|
|
8
8
|
if (fileName.startsWith('video:') && !lc.endsWith('.avi'))
|
|
9
9
|
return fileName.replace('video:', 'file:');
|
|
10
|
-
if (fileName.startsWith('file:') && !lc.endsWith('.avi') && !isMobile)
|
|
11
|
-
return fileName.replace('file:', 'video:');
|
|
12
10
|
if (fileName.startsWith('video:') && lc.endsWith('.avi'))
|
|
13
11
|
return fileName;
|
|
14
12
|
if (fileName.startsWith('file:') || fileName.startsWith('http:') || fileName.startsWith('https:') || fileName.startsWith('/'))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { FileInfo } from "../../../api";
|
|
2
|
+
import { XmlJSON } from "../../../utils/xml";
|
|
1
3
|
import { RouteInfo, RoutePoint } from "../types";
|
|
2
4
|
import { XMLParser, XmlParserContext } from "./xml";
|
|
3
5
|
export interface EnhancedRoutePoint extends RoutePoint {
|
|
@@ -12,6 +14,7 @@ export declare class GPXParser extends XMLParser {
|
|
|
12
14
|
protected static SCHEME: string;
|
|
13
15
|
protected props: GPXParserProps;
|
|
14
16
|
constructor(props?: GPXParserProps);
|
|
17
|
+
getData(file: FileInfo, data?: XmlJSON): Promise<XmlJSON>;
|
|
15
18
|
getPrimaryExtension(): string;
|
|
16
19
|
getCompanionExtensions(): string[];
|
|
17
20
|
supportsExtension(extension: any): boolean;
|
|
@@ -18,6 +18,7 @@ export declare class XMLParser implements Parser<XmlJSON, RouteApiDetail> {
|
|
|
18
18
|
supportsExtension(extension: string): boolean;
|
|
19
19
|
supportsContent(xmljson: XmlJSON): boolean;
|
|
20
20
|
getData(file: FileInfo, data?: XmlJSON): Promise<XmlJSON>;
|
|
21
|
+
protected readAndDecode(file: FileInfo): Promise<string>;
|
|
21
22
|
protected getCountryPrefix(title?: string): string | undefined;
|
|
22
23
|
protected loadDescription(context: XmlParserContext): Promise<void>;
|
|
23
24
|
protected parse(file: FileInfo, xmljson: XmlJSON): Promise<ParseResult<RouteApiDetail>>;
|
|
@@ -30,6 +30,7 @@ export declare class RouteLibraryScannerService extends IncyclistService {
|
|
|
30
30
|
private resolveVideoUri;
|
|
31
31
|
private upsertImportHistory;
|
|
32
32
|
private buildFileInfo;
|
|
33
|
+
private getImportProps;
|
|
33
34
|
private buildRouteDisplayItem;
|
|
34
35
|
private getCompanionExts;
|
|
35
36
|
private getExtension;
|
|
@@ -24,12 +24,14 @@ export interface ParsedRoute {
|
|
|
24
24
|
format: RouteFormat;
|
|
25
25
|
observer?: IObserver;
|
|
26
26
|
}
|
|
27
|
+
export type ParseState = 'waiting' | 'parsing' | 'parsed';
|
|
27
28
|
export interface RouteDisplayItem {
|
|
28
29
|
id: string;
|
|
29
30
|
label: string;
|
|
30
31
|
distance?: FormattedNumber;
|
|
31
32
|
format: RouteFormat;
|
|
32
33
|
alreadyImported: boolean;
|
|
34
|
+
parseState: ParseState;
|
|
33
35
|
importable: boolean;
|
|
34
36
|
errorReason?: string;
|
|
35
37
|
observer: IObserver;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const resolvePlaybackUrl: (url?: string
|
|
1
|
+
export declare const resolvePlaybackUrl: (url?: string) => string | undefined;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incyclist-services",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.137",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"gd-eventlog": "^0.1.27"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@garmin/fitsdk": "^21.213.0",
|
|
9
9
|
"axios": "^1.19.0",
|
|
10
|
-
"incyclist-devices": "^3.0.
|
|
10
|
+
"incyclist-devices": "^3.0.37",
|
|
11
11
|
"promise.any": "^2.0.6",
|
|
12
12
|
"semver": "^7.7.4",
|
|
13
13
|
"tcx-builder": "^1.1.1",
|