2025-02-02 11:17:22 +01:00
|
|
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
|
|
|
import { GPXFileType } from './types';
|
|
|
|
import { GPXFile } from './gpx';
|
2024-04-15 14:26:34 +02:00
|
|
|
|
2025-01-09 20:45:11 +01:00
|
|
|
const attributesWithNamespace = {
|
2025-02-02 11:17:22 +01:00
|
|
|
RoutePointExtension: 'gpxx:RoutePointExtension',
|
|
|
|
rpt: 'gpxx:rpt',
|
|
|
|
TrackPointExtension: 'gpxtpx:TrackPointExtension',
|
|
|
|
PowerExtension: 'gpxpx:PowerExtension',
|
|
|
|
atemp: 'gpxtpx:atemp',
|
|
|
|
hr: 'gpxtpx:hr',
|
|
|
|
cad: 'gpxtpx:cad',
|
|
|
|
Extensions: 'gpxtpx:Extensions',
|
|
|
|
PowerInWatts: 'gpxpx:PowerInWatts',
|
|
|
|
power: 'gpxpx:PowerExtension',
|
|
|
|
line: 'gpx_style:line',
|
|
|
|
color: 'gpx_style:color',
|
|
|
|
opacity: 'gpx_style:opacity',
|
|
|
|
width: 'gpx_style:width',
|
2025-01-09 20:45:11 +01:00
|
|
|
};
|
|
|
|
|
2025-04-04 08:56:53 +02:00
|
|
|
const floatPatterns = [
|
|
|
|
/[-+]?\d*\.\d+$/, // decimal
|
|
|
|
/[-+]?\d+$/, // integer
|
|
|
|
];
|
|
|
|
function safeParseFloat(value: string): number {
|
|
|
|
const parsed = parseFloat(value);
|
|
|
|
if (!isNaN(parsed)) {
|
|
|
|
return parsed;
|
|
|
|
}
|
|
|
|
for (const pattern of floatPatterns) {
|
|
|
|
const match = value.match(pattern);
|
|
|
|
if (match) {
|
|
|
|
return parseFloat(match[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
|
2024-04-15 14:26:34 +02:00
|
|
|
export function parseGPX(gpxData: string): GPXFile {
|
|
|
|
const parser = new XMLParser({
|
|
|
|
ignoreAttributes: false,
|
2025-02-02 11:17:22 +01:00
|
|
|
attributeNamePrefix: '',
|
2024-04-16 13:02:22 +02:00
|
|
|
attributesGroupName: 'attributes',
|
2025-01-09 20:45:11 +01:00
|
|
|
removeNSPrefix: true,
|
2024-07-15 21:47:09 +02:00
|
|
|
isArray(name: string) {
|
2025-02-02 11:17:22 +01:00
|
|
|
return (
|
|
|
|
name === 'trk' ||
|
|
|
|
name === 'trkseg' ||
|
|
|
|
name === 'trkpt' ||
|
|
|
|
name === 'wpt' ||
|
|
|
|
name === 'rte' ||
|
|
|
|
name === 'rtept' ||
|
|
|
|
name === 'gpxx:rpt'
|
|
|
|
);
|
2024-04-16 11:48:42 +02:00
|
|
|
},
|
|
|
|
attributeValueProcessor(attrName, attrValue, jPath) {
|
|
|
|
if (attrName === 'lat' || attrName === 'lon') {
|
2025-04-04 08:56:53 +02:00
|
|
|
return safeParseFloat(attrValue);
|
2024-04-16 11:48:42 +02:00
|
|
|
}
|
|
|
|
return attrValue;
|
|
|
|
},
|
|
|
|
transformTagName(tagName: string) {
|
2025-01-09 20:45:11 +01:00
|
|
|
if (attributesWithNamespace[tagName]) {
|
|
|
|
return attributesWithNamespace[tagName];
|
2025-01-01 20:01:46 +01:00
|
|
|
}
|
2024-04-16 11:48:42 +02:00
|
|
|
return tagName;
|
|
|
|
},
|
2024-07-22 18:27:53 +02:00
|
|
|
parseTagValue: false,
|
2024-04-16 11:48:42 +02:00
|
|
|
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
|
|
|
|
if (isLeafNode) {
|
|
|
|
if (tagName === 'ele') {
|
2025-04-04 08:56:53 +02:00
|
|
|
return safeParseFloat(tagValue);
|
2024-04-16 11:48:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (tagName === 'time') {
|
|
|
|
return new Date(tagValue);
|
|
|
|
}
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
if (
|
|
|
|
tagName === 'gpxtpx:atemp' ||
|
|
|
|
tagName === 'gpxtpx:hr' ||
|
|
|
|
tagName === 'gpxtpx:cad' ||
|
|
|
|
tagName === 'gpxpx:PowerInWatts' ||
|
|
|
|
tagName === 'gpx_style:opacity' ||
|
|
|
|
tagName === 'gpx_style:width'
|
|
|
|
) {
|
2025-04-04 08:56:53 +02:00
|
|
|
return safeParseFloat(tagValue);
|
2024-04-16 11:48:42 +02:00
|
|
|
}
|
|
|
|
|
2024-04-16 13:02:22 +02:00
|
|
|
if (tagName === 'gpxpx:PowerExtension') {
|
2024-04-16 11:48:42 +02:00
|
|
|
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
|
|
|
|
// Note that this only targets the transformed <power> tag, since it must be a leaf node
|
|
|
|
return {
|
2025-04-04 08:56:53 +02:00
|
|
|
'gpxpx:PowerInWatts': safeParseFloat(tagValue),
|
2024-04-16 11:48:42 +02:00
|
|
|
};
|
|
|
|
}
|
2024-04-16 09:54:41 +02:00
|
|
|
}
|
|
|
|
|
2024-04-16 11:48:42 +02:00
|
|
|
return tagValue;
|
|
|
|
},
|
|
|
|
});
|
2024-04-16 09:54:41 +02:00
|
|
|
|
2024-04-16 13:54:48 +02:00
|
|
|
const parsed: GPXFileType = parser.parse(gpxData).gpx;
|
2024-04-16 09:54:41 +02:00
|
|
|
|
2024-07-15 21:47:09 +02:00
|
|
|
// @ts-ignore
|
2025-02-02 11:17:22 +01:00
|
|
|
if (parsed.metadata === '') {
|
2024-07-15 21:47:09 +02:00
|
|
|
parsed.metadata = {};
|
|
|
|
}
|
|
|
|
|
2024-04-16 13:54:48 +02:00
|
|
|
return new GPXFile(parsed);
|
2024-04-16 11:48:42 +02:00
|
|
|
}
|
2024-04-16 13:02:22 +02:00
|
|
|
|
2024-07-23 11:20:31 +02:00
|
|
|
export function buildGPX(file: GPXFile, exclude: string[]): string {
|
2024-09-10 20:17:33 +02:00
|
|
|
const gpx = file.toGPXFileType(exclude);
|
2024-04-19 12:17:31 +02:00
|
|
|
|
2025-02-07 18:13:46 +01:00
|
|
|
let lastDate = undefined;
|
2024-04-16 13:02:22 +02:00
|
|
|
const builder = new XMLBuilder({
|
|
|
|
format: true,
|
|
|
|
ignoreAttributes: false,
|
2025-02-02 11:17:22 +01:00
|
|
|
attributeNamePrefix: '',
|
2024-04-16 13:02:22 +02:00
|
|
|
attributesGroupName: 'attributes',
|
|
|
|
suppressEmptyNode: true,
|
2025-02-07 18:13:46 +01:00
|
|
|
tagValueProcessor: (tagName: string, tagValue: unknown): string | undefined => {
|
2024-04-16 13:02:22 +02:00
|
|
|
if (tagValue instanceof Date) {
|
2025-02-07 18:13:46 +01:00
|
|
|
if (isNaN(tagValue.getTime())) {
|
|
|
|
return lastDate?.toISOString();
|
|
|
|
}
|
|
|
|
lastDate = tagValue;
|
2024-04-16 13:02:22 +02:00
|
|
|
return tagValue.toISOString();
|
|
|
|
}
|
|
|
|
return tagValue.toString();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
if (!gpx.attributes) gpx.attributes = {};
|
2025-01-25 10:47:47 +01:00
|
|
|
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
|
2024-04-16 13:02:22 +02:00
|
|
|
gpx.attributes['version'] = '1.1';
|
|
|
|
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
|
|
|
|
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
|
2025-02-02 11:17:22 +01:00
|
|
|
gpx.attributes['xsi:schemaLocation'] =
|
|
|
|
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
|
2024-04-16 13:02:22 +02:00
|
|
|
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
|
|
|
|
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
|
|
|
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
|
|
|
|
gpx.attributes['xmlns:gpx_style'] = 'http://www.topografix.com/GPX/gpx_style/0/2';
|
|
|
|
|
2024-07-17 23:30:02 +02:00
|
|
|
if (gpx.trk.length === 1 && (gpx.trk[0].name === undefined || gpx.trk[0].name === '')) {
|
|
|
|
gpx.trk[0].name = gpx.metadata.name;
|
|
|
|
}
|
|
|
|
|
2024-04-16 13:02:22 +02:00
|
|
|
return builder.build({
|
2025-02-02 11:17:22 +01:00
|
|
|
'?xml': {
|
2024-04-16 13:02:22 +02:00
|
|
|
attributes: {
|
2025-02-02 11:17:22 +01:00
|
|
|
version: '1.0',
|
|
|
|
encoding: 'UTF-8',
|
|
|
|
},
|
2024-04-16 13:02:22 +02:00
|
|
|
},
|
2025-02-02 11:17:22 +01:00
|
|
|
gpx: removeEmptyElements(gpx),
|
2024-04-16 13:02:22 +02:00
|
|
|
});
|
2024-09-10 16:45:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
|
|
|
for (const key in obj) {
|
2025-02-02 11:17:22 +01:00
|
|
|
if (
|
|
|
|
obj[key] === null ||
|
|
|
|
obj[key] === undefined ||
|
|
|
|
obj[key] === '' ||
|
|
|
|
(Array.isArray(obj[key]) && obj[key].length === 0)
|
|
|
|
) {
|
2024-09-10 16:45:37 +02:00
|
|
|
delete obj[key];
|
2024-09-11 15:08:44 +02:00
|
|
|
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
|
2024-09-10 16:45:37 +02:00
|
|
|
removeEmptyElements(obj[key]);
|
|
|
|
if (Object.keys(obj[key]).length === 0) {
|
|
|
|
delete obj[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return obj;
|
2025-02-02 11:17:22 +01:00
|
|
|
}
|