feat: add package for converting gpx to csv

main
Bjornmossa 1 year ago
parent 0f374c21bb
commit 0bcb76f7bf

@ -0,0 +1,20 @@
/** @module arguments */
/// <reference path="result.js" />
import { ERROR, OK } from "./result.js";
const NO_INPUT_ERROR_MESSAGE = "No file to process. Enter path to file";
/**
* Read argument from nodejs process
* @returns {Result<String>} result of reading argument
*/
function readCliArgument() {
const maybeArgument = process.argv[2];
if (!maybeArgument || maybeArgument.length === 0) {
return [ERROR, NO_INPUT_ERROR_MESSAGE];
}
return [OK, maybeArgument];
}
export { readCliArgument };

@ -0,0 +1,35 @@
/** @module file */
/// <reference path="result.js" />
import { ERROR, OK } from "./result.js";
import { readFile as readF, writeFile as writeF } from "fs/promises";
const FILE_READ_ERROR_MESSAGE = "Error reading file from disk";
const FILE_WRITE_ERROR_MESSAGE = "Error writing file to disk";
/**
* Try to read file from pathname
* @returns {Result<String>} result of reading file
*/
async function readFile(filePath) {
try {
const data = await readF(filePath, "utf8");
return [OK, data];
} catch (err) {
return [ERROR, `${FILE_READ_ERROR_MESSAGE}: ${err}`];
}
}
/**
* Try to write file to disk
* @returns {Result<String>} result of writing file
*/
async function writeFile(filename, content) {
try {
await writeF(filename, content);
return [OK, `File ${filename} created`];
} catch (err) {
return [ERROR, `${FILE_WRITE_ERROR_MESSAGE}: ${err}`];
}
}
export { readFile, writeFile };

@ -0,0 +1,122 @@
/** @module gpx */
/// <reference path="result.js" />
/**
* @typedef {[String, String, String]} Segment
* A tuple of latitude, longtitude and time.
*/
import { parseXml } from "@rgrove/parse-xml";
import { ERROR, OK } from "./result.js";
/**
* Checks if the given JSON object is an element. Utility function for working
* with JsonObject converted from XML.
*
* @param {JsonObject} object object to check.
* @returns {boolean} True if the object.type is an element, false otherwise.
*/
const isElement = (object) => object?.type === "element";
/**
* Checks if the given JSON object is an GPS track object.
* Utility function for working with JsonObject converted from XML.
*
* @param {JsonObject} object to check.
* @returns {boolean} True if the object.name is trk, false otherwise.
*/
const isTrack = (object) => object?.name === "trk";
/**
* Checks if the given JSON object is an GPS track segment object.
* Utility function for working with JsonObject converted from XML.
*
* @param {JsonObject} object to check.
* @returns {boolean} True if the object.name is trkseg, false otherwise.
*/
const isTrackSegment = (object) => object?.name === "trkseg";
/**
* Checks if the given JSON object is an GPS track segment time object.
* Utility function for working with JsonObject converted from XML.
*
* @param {JsonObject} object to check.
* @returns {boolean} True if the object.name is time, false otherwise.
*/
const isSegmentTime = (object) => object?.name === "time";
/**
* Checks if the given JSON object have all nesessary data.
* Utility function for working with JsonObject converted from XML.
*
* @param {JsonObject} object to check.
* @returns {boolean} True if the object have latitude longtitude and time.
*/
const isProperSegment = (object) => {
const haveLat = object?.attributes?.lat !== undefined;
const haveLon = object?.attributes?.lon !== undefined;
const children = object?.children ?? [];
const timeObject = children.filter(isSegmentTime)?.[0];
const haveTime =
timeObject !== undefined && timeObject.children[0].text.length > 0;
return haveLat && haveLon && haveTime;
};
/**
* Take longtitude, latitude and time from JSON object
* and return it as Segment tuple.
*
* @param {JsonObject} object to check.
* @returns {Segment} True if the object have latitude longtitude and time.
*/
const getSegmentData = (object) => {
const segmentTime = object.children.filter(isSegmentTime)[0].children[0].text;
return [object.attributes.lat, object.attributes.lon, segmentTime];
};
/**
* Parse XML document, turn it into JSON and find track segments,
* then transform every object to Segment tuple
*
* @param {String} xmlDoc string representation of XML document.
* @returns {Result<[Segment]>}
*/
function parse(xmlDoc) {
// Check input argument
if (!xmlDoc || xmlDoc.length === 0) {
return [ERROR, "Can not parse empty document"];
}
const gpxObject = parseXml(xmlDoc);
const root = gpxObject.toJSON().children?.[0];
if (!root) {
return [ERROR, "Document doesn't contain root element"];
}
const track = root.children.filter(isElement).filter(isTrack)?.[0];
if (!track) {
return [ERROR, "Document doesn't contain track element"];
}
const segments = track.children
.filter(isElement)
.filter(isTrackSegment)
.reduce(
(seg1, seg2) => [...(seg1?.children ?? []), ...(seg2?.children ?? [])],
[],
)
.filter(isElement)
.filter(isProperSegment)
.map(getSegmentData);
if (segments.length === 0) {
return [ERROR, "There are no track segments to process"];
}
return [OK, segments];
}
export { parse };

@ -0,0 +1,12 @@
/** @module result */
/**
* @template T
* @typedef {[Symbol, T]} Result
* A tuple that contains a Symbol (OK or ERROR) and any value of type T.
*/
const OK = Symbol("OK");
const ERROR = Symbol("ERROR");
export { OK, ERROR };

@ -0,0 +1,53 @@
#!/usr/bin/env node
import { exit } from "process";
import { resolve, basename } from "path";
import { readCliArgument } from "./lib/arguments.js";
import { ERROR } from "./lib/result.js";
import { readFile, writeFile } from "./lib/file.js";
import { parse } from "./lib/gpx.js";
async function main() {
// Read cli argument. Exit if there are no one.
const argumentReadingResult = readCliArgument();
if (argumentReadingResult[0] === ERROR) {
console.error(argumentReadingResult[1]);
exit(1);
}
const filePath = resolve(argumentReadingResult[1]);
// Try to open file. Exit if some errors.
const fileReadingResult = await readFile(filePath);
if (fileReadingResult[0] === ERROR) {
console.error(fileReadingResult[1]);
exit(1);
}
const parsedSegments = parse(fileReadingResult[1]);
if (parsedSegments[0] === ERROR) {
console.error(parsedSegments[1]);
exit(1);
}
const segmentsToCsv = parsedSegments[1]
.map((segment) => segment.join(","))
.join("\n");
const baseFileName = basename(filePath, ".gpx");
const fileName = resolve(`${baseFileName}.csv`);
const writeFileResult = await writeFile(fileName, segmentsToCsv);
if (writeFileResult[0] === ERROR) {
console.error(writeFileResult[1]);
exit(1);
} else {
console.log(writeFileResult[1]);
exit(0);
}
}
main();

28
package-lock.json generated

@ -0,0 +1,28 @@
{
"name": "gpx-sequencer",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gpx-sequencer",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@rgrove/parse-xml": "^4.2.0"
},
"bin": {
"gpx-sequencer": "main.js"
}
},
"node_modules/@rgrove/parse-xml": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.0.tgz",
"integrity": "sha512-UuBOt7BOsKVOkFXRe4Ypd/lADuNIfqJXv8GvHqtXaTYXPPKkj2nS2zPllVsrtRjcomDhIJVBnZwfmlI222WH8g==",
"license": "ISC",
"engines": {
"node": ">=14.0.0"
}
}
}
}

@ -0,0 +1,22 @@
{
"name": "gpx-sequencer",
"version": "1.0.0",
"description": "Tiny project for converting gpx data to other formats thac can be readable with SuperCollider software and used as a source for music composition",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gitea.bjornmossa.net/Bjornmossa/gpx-sequencer.git"
},
"bin": {
"gpx-sequencer": "./main.js"
},
"author": "Bjornmossa",
"license": "GPL-3.0-or-later",
"type": "module",
"dependencies": {
"@rgrove/parse-xml": "^4.2.0"
}
}
Loading…
Cancel
Save