You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 lines
3.6 KiB

/** @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 };