diff --git a/lib/arguments.js b/lib/arguments.js
new file mode 100644
index 0000000..660323b
--- /dev/null
+++ b/lib/arguments.js
@@ -0,0 +1,20 @@
+/** @module arguments */
+///
+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} 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 };
diff --git a/lib/file.js b/lib/file.js
new file mode 100644
index 0000000..4d666d0
--- /dev/null
+++ b/lib/file.js
@@ -0,0 +1,35 @@
+/** @module file */
+///
+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} 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} 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 };
diff --git a/lib/gpx.js b/lib/gpx.js
new file mode 100644
index 0000000..ee9acab
--- /dev/null
+++ b/lib/gpx.js
@@ -0,0 +1,122 @@
+/** @module gpx */
+///
+
+/**
+ * @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 };
diff --git a/lib/result.js b/lib/result.js
new file mode 100644
index 0000000..2304af9
--- /dev/null
+++ b/lib/result.js
@@ -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 };
diff --git a/main.js b/main.js
old mode 100644
new mode 100755
index e69de29..f0203df
--- a/main.js
+++ b/main.js
@@ -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();
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e08d048
--- /dev/null
+++ b/package-lock.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..be7e807
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}