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.
@@ -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 (prevState === 'Finished' || prevState === 'Idle' || prevState === 'Closing') {
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.isMobile() && this.getRideSettingsDisplay().getRideView() === 'sv';
143
+ return this.getRideSettingsDisplay().getRideView() === 'sv';
144
144
  }
145
145
  waitsForSatelliteView() {
146
- return this.isMobile() && this.getRideSettingsDisplay().getRideView() === 'sat';
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, this.isMobile());
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 xml_1 = require("./xml");
9
+ const gpxFast_1 = require("./gpxFast");
10
+ const xml_2 = require("./xml");
9
11
  const MIN_DISTANCE = 1;
10
- class GPXParser extends xml_1.XMLParser {
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.logger.logEvent({ message: 'error', fn: 'loadPoints', reason: 'unvalid point', info });
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('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&quot;', '"').replaceAll('&apos;', "'").replaceAll('&amp;', '&');
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
- gpxFile.filename = gpxFile.filename.replace(xmlBase, fileName);
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 `file://${encodeURI(cleanPath)}`;
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 buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
235
- if (buf[0] === 0xFE && buf[1] === 0xFF) {
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 (buf[0] === 0xFF && buf[1] === 0xFE) {
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 (buf[0] === 0xFD && buf[1] === 0xFD) {
242
- if (buf[2] === 0x00 && buf[3] === 0x3C) {
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 (buf[2] === 0x3C && buf[3] === 0x00) {
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 : buf.toString('utf-8');
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 (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF)
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
- const resData = (0, utils_1.getUtf8Data)(res.data);
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
- const altFound = altitudes.find(a => Number.parseInt(a.distance) === Number.parseInt(pos.distance));
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, isMobile = false) => {
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 (prevState === 'Finished' || prevState === 'Idle' || prevState === 'Closing') {
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.isMobile() && this.getRideSettingsDisplay().getRideView() === 'sv';
140
+ return this.getRideSettingsDisplay().getRideView() === 'sv';
141
141
  }
142
142
  waitsForSatelliteView() {
143
- return this.isMobile() && this.getRideSettingsDisplay().getRideView() === 'sat';
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, this.isMobile());
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.logger.logEvent({ message: 'error', fn: 'loadPoints', reason: 'unvalid point', info });
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('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&quot;', '"').replaceAll('&apos;', "'").replaceAll('&amp;', '&');
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
- gpxFile.filename = gpxFile.filename.replace(xmlBase, fileName);
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 `file://${encodeURI(cleanPath)}`;
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 buf = Buffer.isBuffer(res) ? res : Buffer.from(res, 'binary');
224
- if (buf[0] === 0xFE && buf[1] === 0xFF) {
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 (buf[0] === 0xFF && buf[1] === 0xFE) {
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 (buf[0] === 0xFD && buf[1] === 0xFD) {
231
- if (buf[2] === 0x00 && buf[3] === 0x3C) {
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 (buf[2] === 0x3C && buf[3] === 0x00) {
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 : buf.toString('utf-8');
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 (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF)
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
- const resData = getUtf8Data(res.data);
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
- const altFound = altitudes.find(a => Number.parseInt(a.distance) === Number.parseInt(pos.distance));
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, isMobile = false) => {
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;
@@ -0,0 +1,2 @@
1
+ import { JSONObject } from '../../../utils/xml';
2
+ export declare const tryParseGpxRaw: (xmlText: string) => JSONObject | undefined;
@@ -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, isMobile?: boolean) => string | undefined;
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.136",
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.36",
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",