Files
gpx.studio/gpx/src/io.ts

132 lines
5.4 KiB
TypeScript
Raw Normal View History

2024-04-16 13:02:22 +02:00
import { XMLParser, XMLBuilder } from "fast-xml-parser";
2024-04-16 13:54:48 +02:00
import { GPXFileType } from "./types";
import { GPXFile } from "./gpx";
2024-04-15 14:26:34 +02:00
export function parseGPX(gpxData: string): GPXFile {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "",
2024-04-16 13:02:22 +02:00
attributesGroupName: 'attributes',
isArray(name: string) {
return name === 'trk' || name === 'trkseg' || name === 'trkpt' || name === 'wpt' || name === 'rte' || name === 'rtept' || name === 'gpxx:rpt';
},
attributeValueProcessor(attrName, attrValue, jPath) {
if (attrName === 'lat' || attrName === 'lon') {
return parseFloat(attrValue);
}
return attrValue;
},
transformTagName(tagName: string) {
if (tagName === 'power') {
// Transform the simple <power> tag to the more complex <gpxpx:PowerExtension> tag, the nested <gpxpx:PowerInWatts> tag is then handled by the tagValueProcessor
2024-04-16 13:02:22 +02:00
return 'gpxpx:PowerExtension';
}
// Add the gpx_style namespace to the line style tags
if (tagName === 'line') {
return 'gpx_style:line';
}
if (tagName === 'color') {
return 'gpx_style:color';
}
if (tagName === 'opacity') {
return 'gpx_style:opacity';
}
if (tagName === 'width') {
return 'gpx_style:width';
}
return tagName;
},
2024-07-22 18:27:53 +02:00
parseTagValue: false,
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
if (isLeafNode) {
if (tagName === 'ele') {
return parseFloat(tagValue);
}
if (tagName === 'time') {
return new Date(tagValue);
}
2024-12-28 15:16:32 +01:00
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' ||
tagName === 'gpx_style:opacity' || tagName === 'gpx_style:width') {
return parseFloat(tagValue);
}
2024-04-16 13:02:22 +02:00
if (tagName === 'gpxpx:PowerExtension') {
// 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 {
2024-04-16 13:02:22 +02:00
'gpxpx:PowerInWatts': parseFloat(tagValue)
};
}
2024-04-16 09:54:41 +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
// @ts-ignore
if (parsed.metadata === "") {
parsed.metadata = {};
}
2024-04-16 13:54:48 +02:00
return new GPXFile(parsed);
}
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-16 13:02:22 +02:00
const builder = new XMLBuilder({
format: true,
ignoreAttributes: false,
attributeNamePrefix: "",
attributesGroupName: 'attributes',
suppressEmptyNode: true,
tagValueProcessor: (tagName: string, tagValue: unknown): string => {
if (tagValue instanceof Date) {
return tagValue.toISOString();
}
return tagValue.toString();
},
});
2024-06-18 15:32:54 +02: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';
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';
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';
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({
"?xml": {
attributes: {
version: "1.0",
encoding: "UTF-8",
}
},
2024-09-10 20:17:33 +02: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) {
if (obj[key] === null || obj[key] === undefined || obj[key] === '' || (Array.isArray(obj[key]) && obj[key].length === 0)) {
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;
2024-04-16 13:02:22 +02:00
}