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();
|
||||||
@ -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…
Reference in new issue