mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	export options
This commit is contained in:
		@@ -1,198 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * For a detailed explanation regarding each configuration property, visit:
 | 
			
		||||
 * https://jestjs.io/docs/configuration
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** @type {import('jest').Config} */
 | 
			
		||||
const config = {
 | 
			
		||||
    // All imported modules in your tests should be mocked automatically
 | 
			
		||||
    // automock: false,
 | 
			
		||||
 | 
			
		||||
    // Stop running tests after `n` failures
 | 
			
		||||
    // bail: 0,
 | 
			
		||||
 | 
			
		||||
    // The directory where Jest should store its cached dependency information
 | 
			
		||||
    // cacheDirectory: "/private/var/folders/hf/0lj0fwd15m55qqlzd3d29mtw0000gp/T/jest_dy",
 | 
			
		||||
 | 
			
		||||
    // Automatically clear mock calls, instances, contexts and results before every test
 | 
			
		||||
    // clearMocks: false,
 | 
			
		||||
 | 
			
		||||
    // Indicates whether the coverage information should be collected while executing the test
 | 
			
		||||
    // collectCoverage: false,
 | 
			
		||||
 | 
			
		||||
    // An array of glob patterns indicating a set of files for which coverage information should be collected
 | 
			
		||||
    // collectCoverageFrom: undefined,
 | 
			
		||||
 | 
			
		||||
    // The directory where Jest should output its coverage files
 | 
			
		||||
    // coverageDirectory: undefined,
 | 
			
		||||
 | 
			
		||||
    // An array of regexp pattern strings used to skip coverage collection
 | 
			
		||||
    // coveragePathIgnorePatterns: [
 | 
			
		||||
    //   "/node_modules/"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // Indicates which provider should be used to instrument code for coverage
 | 
			
		||||
    coverageProvider: "v8",
 | 
			
		||||
 | 
			
		||||
    // A list of reporter names that Jest uses when writing coverage reports
 | 
			
		||||
    // coverageReporters: [
 | 
			
		||||
    //   "json",
 | 
			
		||||
    //   "text",
 | 
			
		||||
    //   "lcov",
 | 
			
		||||
    //   "clover"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // An object that configures minimum threshold enforcement for coverage results
 | 
			
		||||
    // coverageThreshold: undefined,
 | 
			
		||||
 | 
			
		||||
    // A path to a custom dependency extractor
 | 
			
		||||
    // dependencyExtractor: undefined,
 | 
			
		||||
 | 
			
		||||
    // Make calling deprecated APIs throw helpful error messages
 | 
			
		||||
    // errorOnDeprecated: false,
 | 
			
		||||
 | 
			
		||||
    // The default configuration for fake timers
 | 
			
		||||
    // fakeTimers: {
 | 
			
		||||
    //   "enableGlobally": false
 | 
			
		||||
    // },
 | 
			
		||||
 | 
			
		||||
    // Force coverage collection from ignored files using an array of glob patterns
 | 
			
		||||
    // forceCoverageMatch: [],
 | 
			
		||||
 | 
			
		||||
    // A path to a module which exports an async function that is triggered once before all test suites
 | 
			
		||||
    // globalSetup: undefined,
 | 
			
		||||
 | 
			
		||||
    // A path to a module which exports an async function that is triggered once after all test suites
 | 
			
		||||
    // globalTeardown: undefined,
 | 
			
		||||
 | 
			
		||||
    // A set of global variables that need to be available in all test environments
 | 
			
		||||
    // globals: {},
 | 
			
		||||
 | 
			
		||||
    // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
 | 
			
		||||
    // maxWorkers: "50%",
 | 
			
		||||
 | 
			
		||||
    // An array of directory names to be searched recursively up from the requiring module's location
 | 
			
		||||
    // moduleDirectories: [
 | 
			
		||||
    //   "node_modules"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // An array of file extensions your modules use
 | 
			
		||||
    // moduleFileExtensions: [
 | 
			
		||||
    //   "js",
 | 
			
		||||
    //   "mjs",
 | 
			
		||||
    //   "cjs",
 | 
			
		||||
    //   "jsx",
 | 
			
		||||
    //   "ts",
 | 
			
		||||
    //   "tsx",
 | 
			
		||||
    //   "json",
 | 
			
		||||
    //   "node"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
 | 
			
		||||
    // moduleNameMapper: {},
 | 
			
		||||
 | 
			
		||||
    // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
 | 
			
		||||
    // modulePathIgnorePatterns: [],
 | 
			
		||||
 | 
			
		||||
    // Activates notifications for test results
 | 
			
		||||
    // notify: false,
 | 
			
		||||
 | 
			
		||||
    // An enum that specifies notification mode. Requires { notify: true }
 | 
			
		||||
    // notifyMode: "failure-change",
 | 
			
		||||
 | 
			
		||||
    // A preset that is used as a base for Jest's configuration
 | 
			
		||||
    preset: "ts-jest",
 | 
			
		||||
 | 
			
		||||
    // Run tests from one or more projects
 | 
			
		||||
    // projects: undefined,
 | 
			
		||||
 | 
			
		||||
    // Use this configuration option to add custom reporters to Jest
 | 
			
		||||
    // reporters: undefined,
 | 
			
		||||
 | 
			
		||||
    // Automatically reset mock state before every test
 | 
			
		||||
    // resetMocks: false,
 | 
			
		||||
 | 
			
		||||
    // Reset the module registry before running each individual test
 | 
			
		||||
    // resetModules: false,
 | 
			
		||||
 | 
			
		||||
    // A path to a custom resolver
 | 
			
		||||
    // resolver: undefined,
 | 
			
		||||
 | 
			
		||||
    // Automatically restore mock state and implementation before every test
 | 
			
		||||
    // restoreMocks: false,
 | 
			
		||||
 | 
			
		||||
    // The root directory that Jest should scan for tests and modules within
 | 
			
		||||
    rootDir: "test",
 | 
			
		||||
 | 
			
		||||
    // A list of paths to directories that Jest should use to search for files in
 | 
			
		||||
    // roots: [
 | 
			
		||||
    //   "<rootDir>"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // Allows you to use a custom runner instead of Jest's default test runner
 | 
			
		||||
    // runner: "jest-runner",
 | 
			
		||||
 | 
			
		||||
    // The paths to modules that run some code to configure or set up the testing environment before each test
 | 
			
		||||
    // setupFiles: [],
 | 
			
		||||
 | 
			
		||||
    // A list of paths to modules that run some code to configure or set up the testing framework before each test
 | 
			
		||||
    // setupFilesAfterEnv: [],
 | 
			
		||||
 | 
			
		||||
    // The number of seconds after which a test is considered as slow and reported as such in the results.
 | 
			
		||||
    // slowTestThreshold: 5,
 | 
			
		||||
 | 
			
		||||
    // A list of paths to snapshot serializer modules Jest should use for snapshot testing
 | 
			
		||||
    // snapshotSerializers: [],
 | 
			
		||||
 | 
			
		||||
    // The test environment that will be used for testing
 | 
			
		||||
    // testEnvironment: "jest-environment-node",
 | 
			
		||||
 | 
			
		||||
    // Options that will be passed to the testEnvironment
 | 
			
		||||
    // testEnvironmentOptions: {},
 | 
			
		||||
 | 
			
		||||
    // Adds a location field to test results
 | 
			
		||||
    // testLocationInResults: false,
 | 
			
		||||
 | 
			
		||||
    // The glob patterns Jest uses to detect test files
 | 
			
		||||
    // testMatch: [
 | 
			
		||||
    //   "**/__tests__/**/*.[jt]s?(x)",
 | 
			
		||||
    //   "**/?(*.)+(spec|test).[tj]s?(x)"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
 | 
			
		||||
    // testPathIgnorePatterns: [
 | 
			
		||||
    //   "/node_modules/"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // The regexp pattern or array of patterns that Jest uses to detect test files
 | 
			
		||||
    // testRegex: [],
 | 
			
		||||
 | 
			
		||||
    // This option allows the use of a custom results processor
 | 
			
		||||
    // testResultsProcessor: undefined,
 | 
			
		||||
 | 
			
		||||
    // This option allows use of a custom test runner
 | 
			
		||||
    // testRunner: "jest-circus/runner",
 | 
			
		||||
 | 
			
		||||
    // A map from regular expressions to paths to transformers
 | 
			
		||||
    // transform: undefined,
 | 
			
		||||
 | 
			
		||||
    // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
 | 
			
		||||
    // transformIgnorePatterns: [
 | 
			
		||||
    //   "/node_modules/",
 | 
			
		||||
    //   "\\.pnp\\.[^\\/]+$"
 | 
			
		||||
    // ],
 | 
			
		||||
 | 
			
		||||
    // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
 | 
			
		||||
    // unmockedModulePathPatterns: undefined,
 | 
			
		||||
 | 
			
		||||
    // Indicates whether each individual test should be reported during the run
 | 
			
		||||
    // verbose: undefined,
 | 
			
		||||
 | 
			
		||||
    // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
 | 
			
		||||
    // watchPathIgnorePatterns: [],
 | 
			
		||||
 | 
			
		||||
    // Whether to use watchman for file crawling
 | 
			
		||||
    // watchman: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										3615
									
								
								gpx/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3615
									
								
								gpx/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -2,8 +2,8 @@
 | 
			
		||||
    "name": "gpx",
 | 
			
		||||
    "version": "1.0.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "exports": "./dist/src/index.js",
 | 
			
		||||
    "types": "dist/src/index.d.ts",
 | 
			
		||||
    "exports": "./dist/index.js",
 | 
			
		||||
    "types": "dist/index.d.ts",
 | 
			
		||||
    "repository": {
 | 
			
		||||
        "type": "git",
 | 
			
		||||
        "url": "https://github.com/gpxstudio/gpx.studio.git",
 | 
			
		||||
@@ -16,15 +16,11 @@
 | 
			
		||||
        "ts-node": "^10.9.2"
 | 
			
		||||
    },
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "build": "tsc",
 | 
			
		||||
        "test": "jest"
 | 
			
		||||
        "build": "tsc"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@types/geojson": "^7946.0.14",
 | 
			
		||||
        "@types/jest": "^29.5.12",
 | 
			
		||||
        "@types/node": "^20.14.6",
 | 
			
		||||
        "jest": "^29.7.0",
 | 
			
		||||
        "ts-jest": "^29.1.5",
 | 
			
		||||
        "typescript": "^5.4.5"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								gpx/src/gpx.ts
									
									
									
									
									
								
							
							
						
						
									
										137
									
								
								gpx/src/gpx.ts
									
									
									
									
									
								
							@@ -212,14 +212,32 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toGPXFileType(): GPXFileType {
 | 
			
		||||
        return {
 | 
			
		||||
    toGPXFileType(exclude: string[] = []): GPXFileType {
 | 
			
		||||
        let file: GPXFileType = {
 | 
			
		||||
            attributes: cloneJSON(this.attributes),
 | 
			
		||||
            metadata: cloneJSON(this.metadata),
 | 
			
		||||
            metadata: {},
 | 
			
		||||
            wpt: this.wpt,
 | 
			
		||||
            trk: this.trk.map((track) => track.toTrackType()),
 | 
			
		||||
            trk: this.trk.map((track) => track.toTrackType(exclude)),
 | 
			
		||||
            rte: [],
 | 
			
		||||
        };
 | 
			
		||||
        if (this.metadata) {
 | 
			
		||||
            if (this.metadata.name) {
 | 
			
		||||
                file.metadata.name = this.metadata.name;
 | 
			
		||||
            }
 | 
			
		||||
            if (this.metadata.desc) {
 | 
			
		||||
                file.metadata.desc = this.metadata.desc;
 | 
			
		||||
            }
 | 
			
		||||
            if (this.metadata.author) {
 | 
			
		||||
                file.metadata.author = cloneJSON(this.metadata.author);
 | 
			
		||||
            }
 | 
			
		||||
            if (this.metadata.link) {
 | 
			
		||||
                file.metadata.link = cloneJSON(this.metadata.link);
 | 
			
		||||
            }
 | 
			
		||||
            if (this.metadata.time && !exclude.includes('time')) {
 | 
			
		||||
                file.metadata.time = this.metadata.time;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return file;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Producers
 | 
			
		||||
@@ -475,7 +493,7 @@ export class Track extends GPXTreeNode<TrackSegment> {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toTrackType(): TrackType {
 | 
			
		||||
    toTrackType(exclude: string[] = []): TrackType {
 | 
			
		||||
        return {
 | 
			
		||||
            name: this.name,
 | 
			
		||||
            cmt: this.cmt,
 | 
			
		||||
@@ -483,7 +501,7 @@ export class Track extends GPXTreeNode<TrackSegment> {
 | 
			
		||||
            src: this.src,
 | 
			
		||||
            link: this.link,
 | 
			
		||||
            type: this.type,
 | 
			
		||||
            trkseg: this.trkseg.map((seg) => seg.toTrackSegmentType()),
 | 
			
		||||
            trkseg: this.trkseg.map((seg) => seg.toTrackSegmentType(exclude)),
 | 
			
		||||
            extensions: this.extensions,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
@@ -678,6 +696,30 @@ export class TrackSegment extends GPXTreeLeaf {
 | 
			
		||||
            statistics.global.bounds.southWest.lon = Math.min(statistics.global.bounds.southWest.lon, points[i].attributes.lon);
 | 
			
		||||
            statistics.global.bounds.northEast.lat = Math.max(statistics.global.bounds.northEast.lat, points[i].attributes.lat);
 | 
			
		||||
            statistics.global.bounds.northEast.lon = Math.max(statistics.global.bounds.northEast.lon, points[i].attributes.lon);
 | 
			
		||||
 | 
			
		||||
            // extensions
 | 
			
		||||
            if (points[i].extensions) {
 | 
			
		||||
                if (points[i].extensions["gpxtpx:TrackPointExtension"] && points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"]) {
 | 
			
		||||
                    let hr = points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"];
 | 
			
		||||
                    statistics.global.hr.avg = (statistics.global.hr.count * statistics.global.hr.avg + hr) / (statistics.global.hr.count + 1);
 | 
			
		||||
                    statistics.global.hr.count++;
 | 
			
		||||
                }
 | 
			
		||||
                if (points[i].extensions["gpxtpx:TrackPointExtension"] && points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"]) {
 | 
			
		||||
                    let cad = points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"];
 | 
			
		||||
                    statistics.global.cad.avg = (statistics.global.cad.count * statistics.global.cad.avg + cad) / (statistics.global.cad.count + 1);
 | 
			
		||||
                    statistics.global.cad.count++;
 | 
			
		||||
                }
 | 
			
		||||
                if (points[i].extensions["gpxtpx:TrackPointExtension"] && points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"]) {
 | 
			
		||||
                    let atemp = points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"];
 | 
			
		||||
                    statistics.global.atemp.avg = (statistics.global.atemp.count * statistics.global.atemp.avg + atemp) / (statistics.global.atemp.count + 1);
 | 
			
		||||
                    statistics.global.atemp.count++;
 | 
			
		||||
                }
 | 
			
		||||
                if (points[i].extensions["gpxpx:PowerExtension"] && points[i].extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"]) {
 | 
			
		||||
                    let power = points[i].extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"];
 | 
			
		||||
                    statistics.global.power.avg = (statistics.global.power.count * statistics.global.power.avg + power) / (statistics.global.power.count + 1);
 | 
			
		||||
                    statistics.global.power.count++;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [statistics.local.slope.segment, statistics.local.slope.length] = this._computeSlopeSegments(statistics);
 | 
			
		||||
@@ -790,9 +832,9 @@ export class TrackSegment extends GPXTreeLeaf {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toTrackSegmentType(): TrackSegmentType {
 | 
			
		||||
    toTrackSegmentType(exclude: string[] = []): TrackSegmentType {
 | 
			
		||||
        return {
 | 
			
		||||
            trkpt: this.trkpt.map((point) => point.toTrackPointType())
 | 
			
		||||
            trkpt: this.trkpt.map((point) => point.toTrackPointType(exclude))
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -975,13 +1017,38 @@ export class TrackPoint {
 | 
			
		||||
        this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]["surface"] = surface;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toTrackPointType(): TrackPointType {
 | 
			
		||||
        return {
 | 
			
		||||
    toTrackPointType(exclude: string[] = []): TrackPointType {
 | 
			
		||||
        let trkpt: TrackPointType = {
 | 
			
		||||
            attributes: this.attributes,
 | 
			
		||||
            ele: this.ele,
 | 
			
		||||
            time: this.time,
 | 
			
		||||
            extensions: this.extensions,
 | 
			
		||||
        };
 | 
			
		||||
        if (!exclude.includes('time')) {
 | 
			
		||||
            trkpt = { ...trkpt, time: this.time };
 | 
			
		||||
        }
 | 
			
		||||
        if (this.extensions) {
 | 
			
		||||
            trkpt = {
 | 
			
		||||
                ...trkpt, extensions: {
 | 
			
		||||
                    "gpxtpx:TrackPointExtension": {},
 | 
			
		||||
                    "gpxpx:PowerExtension": {},
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"] && !exclude.includes('hr')) {
 | 
			
		||||
                trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"];
 | 
			
		||||
            }
 | 
			
		||||
            if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"] && !exclude.includes('cad')) {
 | 
			
		||||
                trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"];
 | 
			
		||||
            }
 | 
			
		||||
            if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"] && !exclude.includes('atemp')) {
 | 
			
		||||
                trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"];
 | 
			
		||||
            }
 | 
			
		||||
            if (this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] && !exclude.includes('power')) {
 | 
			
		||||
                trkpt.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] = this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"];
 | 
			
		||||
            }
 | 
			
		||||
            if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface && !exclude.includes('surface')) {
 | 
			
		||||
                trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] = { surface: this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return trkpt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clone(): TrackPoint {
 | 
			
		||||
@@ -1084,6 +1151,22 @@ export class GPXStatistics {
 | 
			
		||||
            southWest: Coordinates,
 | 
			
		||||
            northEast: Coordinates,
 | 
			
		||||
        },
 | 
			
		||||
        hr: {
 | 
			
		||||
            avg: number,
 | 
			
		||||
            count: number,
 | 
			
		||||
        },
 | 
			
		||||
        cad: {
 | 
			
		||||
            avg: number,
 | 
			
		||||
            count: number,
 | 
			
		||||
        },
 | 
			
		||||
        atemp: {
 | 
			
		||||
            avg: number,
 | 
			
		||||
            count: number,
 | 
			
		||||
        },
 | 
			
		||||
        power: {
 | 
			
		||||
            avg: number,
 | 
			
		||||
            count: number,
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    local: {
 | 
			
		||||
        points: TrackPoint[],
 | 
			
		||||
@@ -1138,6 +1221,22 @@ export class GPXStatistics {
 | 
			
		||||
                    lon: -180,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            hr: {
 | 
			
		||||
                avg: 0,
 | 
			
		||||
                count: 0,
 | 
			
		||||
            },
 | 
			
		||||
            cad: {
 | 
			
		||||
                avg: 0,
 | 
			
		||||
                count: 0,
 | 
			
		||||
            },
 | 
			
		||||
            atemp: {
 | 
			
		||||
                avg: 0,
 | 
			
		||||
                count: 0,
 | 
			
		||||
            },
 | 
			
		||||
            power: {
 | 
			
		||||
                avg: 0,
 | 
			
		||||
                count: 0,
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        this.local = {
 | 
			
		||||
            points: [],
 | 
			
		||||
@@ -1198,6 +1297,15 @@ export class GPXStatistics {
 | 
			
		||||
        this.global.bounds.southWest.lon = Math.min(this.global.bounds.southWest.lon, other.global.bounds.southWest.lon);
 | 
			
		||||
        this.global.bounds.northEast.lat = Math.max(this.global.bounds.northEast.lat, other.global.bounds.northEast.lat);
 | 
			
		||||
        this.global.bounds.northEast.lon = Math.max(this.global.bounds.northEast.lon, other.global.bounds.northEast.lon);
 | 
			
		||||
 | 
			
		||||
        this.global.hr.avg = (this.global.hr.count * this.global.hr.avg + other.global.hr.count * other.global.hr.avg) / Math.max(1, this.global.hr.count + other.global.hr.count);
 | 
			
		||||
        this.global.hr.count += other.global.hr.count;
 | 
			
		||||
        this.global.cad.avg = (this.global.cad.count * this.global.cad.avg + other.global.cad.count * other.global.cad.avg) / Math.max(1, this.global.cad.count + other.global.cad.count);
 | 
			
		||||
        this.global.cad.count += other.global.cad.count;
 | 
			
		||||
        this.global.atemp.avg = (this.global.atemp.count * this.global.atemp.avg + other.global.atemp.count * other.global.atemp.avg) / Math.max(1, this.global.atemp.count + other.global.atemp.count);
 | 
			
		||||
        this.global.atemp.count += other.global.atemp.count;
 | 
			
		||||
        this.global.power.avg = (this.global.power.count * this.global.power.avg + other.global.power.count * other.global.power.avg) / Math.max(1, this.global.power.count + other.global.power.count);
 | 
			
		||||
        this.global.power.count += other.global.power.count;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    slice(start: number, end: number): GPXStatistics {
 | 
			
		||||
@@ -1225,6 +1333,11 @@ export class GPXStatistics {
 | 
			
		||||
        statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
 | 
			
		||||
        statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
 | 
			
		||||
 | 
			
		||||
        statistics.global.hr = this.global.hr;
 | 
			
		||||
        statistics.global.cad = this.global.cad;
 | 
			
		||||
        statistics.global.atemp = this.global.atemp;
 | 
			
		||||
        statistics.global.power = this.global.power;
 | 
			
		||||
 | 
			
		||||
        return statistics;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -61,9 +61,8 @@ export function parseGPX(gpxData: string): GPXFile {
 | 
			
		||||
    return new GPXFile(parsed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function buildGPX(file: GPXFile): string {
 | 
			
		||||
    const gpx = file.toGPXFileType();
 | 
			
		||||
export function buildGPX(file: GPXFile, exclude: string[]): string {
 | 
			
		||||
    const gpx = file.toGPXFileType(exclude);
 | 
			
		||||
 | 
			
		||||
    const builder = new XMLBuilder({
 | 
			
		||||
        format: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
 | 
			
		||||
import { parseGPX, buildGPX } from '../src/io';
 | 
			
		||||
 | 
			
		||||
describe('GPX operations', () => {
 | 
			
		||||
    it('Clone', () => {
 | 
			
		||||
        const path = "test-data/with_tracks_and_segments.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const cloned = original.clone();
 | 
			
		||||
 | 
			
		||||
        expect(cloned).not.toBe(original);
 | 
			
		||||
 | 
			
		||||
        const originalString = buildGPX(original);
 | 
			
		||||
        const clonedString = buildGPX(cloned);
 | 
			
		||||
 | 
			
		||||
        expect(clonedString).toBe(originalString);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('Reverse', () => {
 | 
			
		||||
        const path = "test-data/with_tracks_and_segments.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        let reversed = original.clone();
 | 
			
		||||
        reversed.reverse();
 | 
			
		||||
 | 
			
		||||
        expect(original.getStartTimestamp().getTime()).toBe(reversed.getStartTimestamp().getTime());
 | 
			
		||||
        expect(original.getEndTimestamp().getTime()).toBe(reversed.getEndTimestamp().getTime());
 | 
			
		||||
 | 
			
		||||
        expect(reversed.trk.length).toBe(original.trk.length);
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < original.trk.length; i++) {
 | 
			
		||||
            const originalTrack = original.trk[i];
 | 
			
		||||
            const reversedTrack = reversed.trk[original.trk.length - i - 1];
 | 
			
		||||
 | 
			
		||||
            expect(reversedTrack.trkseg.length).toBe(originalTrack.trkseg.length);
 | 
			
		||||
 | 
			
		||||
            for (let j = 0; j < originalTrack.trkseg.length; j++) {
 | 
			
		||||
                const originalSegment = originalTrack.trkseg[j];
 | 
			
		||||
                const reversedSegment = reversedTrack.trkseg[originalTrack.trkseg.length - j - 1];
 | 
			
		||||
 | 
			
		||||
                expect(reversedSegment.trkpt.length).toBe(originalSegment.trkpt.length);
 | 
			
		||||
 | 
			
		||||
                for (let k = 0; k < originalSegment.trkpt.length; k++) {
 | 
			
		||||
                    const originalPoint = originalSegment.trkpt[k];
 | 
			
		||||
                    const reversedPoint = reversedSegment.trkpt[originalSegment.trkpt.length - k - 1];
 | 
			
		||||
 | 
			
		||||
                    expect(reversedPoint.attributes.lat).toBe(originalPoint.attributes.lat);
 | 
			
		||||
                    expect(reversedPoint.attributes.lon).toBe(originalPoint.attributes.lon);
 | 
			
		||||
                    expect(reversedPoint.ele).toBe(originalPoint.ele);
 | 
			
		||||
 | 
			
		||||
                    expect(reversed.getEndTimestamp().getTime() - reversedPoint.time.getTime()).toBe(originalPoint.time.getTime() - original.getStartTimestamp().getTime());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,364 +0,0 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
 | 
			
		||||
import { parseGPX, buildGPX } from '../src/io';
 | 
			
		||||
 | 
			
		||||
describe("Parsing", () => {
 | 
			
		||||
    it("Simple", () => {
 | 
			
		||||
        const path = "test-data/simple.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        expect(result.attributes.creator).toBe("https://gpx.studio");
 | 
			
		||||
        expect(result.metadata.name).toBe("simple");
 | 
			
		||||
        expect(result.metadata.author.name).toBe("gpx.studio");
 | 
			
		||||
        expect(result.metadata.author.link.attributes.href).toBe("https://gpx.studio");
 | 
			
		||||
 | 
			
		||||
        expect(result.trk.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        expect(track.name).toBe("simple");
 | 
			
		||||
        expect(track.type).toBe("Cycling");
 | 
			
		||||
        expect(track.trkseg.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
        expect(segment.trkpt.length).toBe(80);
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            const point = segment.trkpt[i];
 | 
			
		||||
            expect(point).toHaveProperty('attributes');
 | 
			
		||||
            expect(point.attributes).toHaveProperty('lat');
 | 
			
		||||
            expect(point.attributes).toHaveProperty('lon');
 | 
			
		||||
            expect(point).toHaveProperty('ele');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].attributes.lat).toBe(50.790867);
 | 
			
		||||
        expect(segment.trkpt[0].attributes.lon).toBe(4.404968);
 | 
			
		||||
        expect(segment.trkpt[0].ele).toBe(109.0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Multiple tracks", () => {
 | 
			
		||||
        const path = "test-data/with_tracks.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        expect(result.trk.length).toBe(2);
 | 
			
		||||
 | 
			
		||||
        const track_1 = result.trk[0];
 | 
			
		||||
        expect(track_1.name).toBe("track 1");
 | 
			
		||||
        expect(track_1.trkseg.length).toBe(1);
 | 
			
		||||
        expect(track_1.trkseg[0].trkpt.length).toBe(49);
 | 
			
		||||
 | 
			
		||||
        const track_2 = result.trk[1];
 | 
			
		||||
        expect(track_2.name).toBe("track 2");
 | 
			
		||||
        expect(track_2.trkseg.length).toBe(1);
 | 
			
		||||
        expect(track_2.trkseg[0].trkpt.length).toBe(28);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Multiple segments", () => {
 | 
			
		||||
        const path = "test-data/with_segments.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        expect(result.trk.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        expect(track.trkseg.length).toBe(2);
 | 
			
		||||
        expect(track.trkseg[0].trkpt.length).toBe(49);
 | 
			
		||||
        expect(track.trkseg[1].trkpt.length).toBe(28);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Waypoint", () => {
 | 
			
		||||
        const path = "test-data/with_waypoint.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        expect(result.wpt.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
        const waypoint = result.wpt[0];
 | 
			
		||||
        expect(waypoint.attributes.lat).toBe(50.7836710064975);
 | 
			
		||||
        expect(waypoint.attributes.lon).toBe(4.410764082658738);
 | 
			
		||||
        expect(waypoint.ele).toBe(122.0);
 | 
			
		||||
        expect(waypoint.name).toBe("Waypoint");
 | 
			
		||||
        expect(waypoint.cmt).toBe("Comment");
 | 
			
		||||
        expect(waypoint.desc).toBe("Description");
 | 
			
		||||
        expect(waypoint.sym).toBe("Bike Trail");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Time", () => {
 | 
			
		||||
        const path = "test-data/with_time.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            expect(segment.trkpt[i].time).toBeInstanceOf(Date);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].time).toEqual(new Date("2023-12-31T23:00:00.000Z"));
 | 
			
		||||
        expect(segment.trkpt[segment.trkpt.length - 1].time).toEqual(new Date("2023-12-31T23:06:40.567Z"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Heart rate", () => {
 | 
			
		||||
        const path = "test-data/with_hr.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            expect(segment.trkpt[i]).toHaveProperty('extensions');
 | 
			
		||||
            expect(segment.trkpt[i].extensions).toHaveProperty('gpxtpx:TrackPointExtension');
 | 
			
		||||
            expect(segment.trkpt[i].extensions['gpxtpx:TrackPointExtension']).toHaveProperty('gpxtpx:hr');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr']).toBe(150);
 | 
			
		||||
        expect(segment.trkpt[segment.trkpt.length - 1].extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr']).toBe(160);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Cadence", () => {
 | 
			
		||||
        const path = "test-data/with_cad.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            expect(segment.trkpt[i]).toHaveProperty('extensions');
 | 
			
		||||
            expect(segment.trkpt[i].extensions).toHaveProperty('gpxtpx:TrackPointExtension');
 | 
			
		||||
            expect(segment.trkpt[i].extensions['gpxtpx:TrackPointExtension']).toHaveProperty('gpxtpx:cad');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad']).toBe(80);
 | 
			
		||||
        expect(segment.trkpt[segment.trkpt.length - 1].extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad']).toBe(90);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Temperature", () => {
 | 
			
		||||
        const path = "test-data/with_temp.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            expect(segment.trkpt[i]).toHaveProperty('extensions');
 | 
			
		||||
            expect(segment.trkpt[i].extensions).toHaveProperty('gpxtpx:TrackPointExtension');
 | 
			
		||||
            expect(segment.trkpt[i].extensions['gpxtpx:TrackPointExtension']).toHaveProperty('gpxtpx:atemp');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp']).toBe(21);
 | 
			
		||||
        expect(segment.trkpt[segment.trkpt.length - 1].extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp']).toBe(22);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Power 1", () => {
 | 
			
		||||
        const path = "test-data/with_power_1.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            expect(segment.trkpt[i]).toHaveProperty('extensions');
 | 
			
		||||
            expect(segment.trkpt[i].extensions).toHaveProperty('gpxpx:PowerExtension');
 | 
			
		||||
            expect(segment.trkpt[i].extensions['gpxpx:PowerExtension']).toHaveProperty('gpxpx:PowerInWatts');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].extensions['gpxpx:PowerExtension']['gpxpx:PowerInWatts']).toBe(200);
 | 
			
		||||
        expect(segment.trkpt[segment.trkpt.length - 1].extensions['gpxpx:PowerExtension']['gpxpx:PowerInWatts']).toBe(210);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Power 2", () => {
 | 
			
		||||
        const path = "test-data/with_power_2.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            expect(segment.trkpt[i]).toHaveProperty('extensions');
 | 
			
		||||
            expect(segment.trkpt[i].extensions).toHaveProperty('gpxpx:PowerExtension');
 | 
			
		||||
            expect(segment.trkpt[i].extensions['gpxpx:PowerExtension']).toHaveProperty('gpxpx:PowerInWatts');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].extensions['gpxpx:PowerExtension']['gpxpx:PowerInWatts']).toBe(200);
 | 
			
		||||
        expect(segment.trkpt[segment.trkpt.length - 1].extensions['gpxpx:PowerExtension']['gpxpx:PowerInWatts']).toBe(210);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Surface", () => {
 | 
			
		||||
        const path = "test-data/with_surface.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
        const segment = track.trkseg[0];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < segment.trkpt.length; i++) {
 | 
			
		||||
            expect(segment.trkpt[i]).toHaveProperty('extensions');
 | 
			
		||||
            expect(segment.trkpt[i].extensions).toHaveProperty('gpxtpx:TrackPointExtension');
 | 
			
		||||
            expect(segment.trkpt[i].extensions['gpxtpx:TrackPointExtension']).toHaveProperty('gpxtpx:Extensions');
 | 
			
		||||
            expect(segment.trkpt[i].extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']).toHaveProperty('surface');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(segment.trkpt[0].extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'].surface).toBe("asphalt");
 | 
			
		||||
        expect(segment.trkpt[segment.trkpt.length - 1].extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'].surface).toBe("cobblestone");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Track style", () => {
 | 
			
		||||
        const path = "test-data/with_style.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const result = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const track = result.trk[0];
 | 
			
		||||
 | 
			
		||||
        expect(track).toHaveProperty('extensions');
 | 
			
		||||
        expect(track.extensions).toHaveProperty('gpx_style:line');
 | 
			
		||||
 | 
			
		||||
        expect(track.extensions['gpx_style:line']).toHaveProperty('color');
 | 
			
		||||
        expect(track.extensions['gpx_style:line']).toHaveProperty('opacity');
 | 
			
		||||
        expect(track.extensions['gpx_style:line']).toHaveProperty('weight');
 | 
			
		||||
 | 
			
		||||
        expect(track.extensions['gpx_style:line'].color).toBe("#2d3ee9");
 | 
			
		||||
        expect(track.extensions['gpx_style:line'].opacity).toBe(0.5);
 | 
			
		||||
        expect(track.extensions['gpx_style:line'].weight).toBe(6);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("Building", () => {
 | 
			
		||||
    it("Simple", () => {
 | 
			
		||||
        const path = "test-data/simple.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Multiple tracks", () => {
 | 
			
		||||
        const path = "test-data/with_tracks.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Multiple segments", () => {
 | 
			
		||||
        const path = "test-data/with_segments.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Waypoint", () => {
 | 
			
		||||
        const path = "test-data/with_waypoint.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Time", () => {
 | 
			
		||||
        const path = "test-data/with_time.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Heart rate", () => {
 | 
			
		||||
        const path = "test-data/with_hr.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Cadence", () => {
 | 
			
		||||
        const path = "test-data/with_cad.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Temperature", () => {
 | 
			
		||||
        const path = "test-data/with_temp.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Power 1", () => {
 | 
			
		||||
        const path = "test-data/with_power_1.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Power 2", () => {
 | 
			
		||||
        const path = "test-data/with_power_2.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Surface", () => {
 | 
			
		||||
        const path = "test-data/with_surface.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Track style", () => {
 | 
			
		||||
        const path = "test-data/with_style.gpx";
 | 
			
		||||
        const data = fs.readFileSync(path, 'utf8');
 | 
			
		||||
        const original = parseGPX(data);
 | 
			
		||||
 | 
			
		||||
        const built = buildGPX(original);
 | 
			
		||||
        const rebuilt = parseGPX(built);
 | 
			
		||||
 | 
			
		||||
        expect(rebuilt).toEqual(original);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
        "moduleResolution": "node",
 | 
			
		||||
    },
 | 
			
		||||
    "include": [
 | 
			
		||||
        "src",
 | 
			
		||||
        "test"
 | 
			
		||||
        "src"
 | 
			
		||||
    ],
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -71,10 +71,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "devDependencies": {
 | 
			
		||||
                "@types/geojson": "^7946.0.14",
 | 
			
		||||
                "@types/jest": "^29.5.12",
 | 
			
		||||
                "@types/node": "^20.14.6",
 | 
			
		||||
                "jest": "^29.7.0",
 | 
			
		||||
                "ts-jest": "^29.1.5",
 | 
			
		||||
                "typescript": "^5.4.5"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -67,4 +67,4 @@
 | 
			
		||||
        "tailwind-merge": "^2.3.0",
 | 
			
		||||
        "tailwind-variants": "^0.2.1"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,24 +1,75 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import { Label } from '$lib/components/ui/label';
 | 
			
		||||
	import { Checkbox } from '$lib/components/ui/checkbox';
 | 
			
		||||
	import { Separator } from '$lib/components/ui/separator';
 | 
			
		||||
	import { Dialog } from 'bits-ui';
 | 
			
		||||
	import {
 | 
			
		||||
		currentTool,
 | 
			
		||||
		exportAllFiles,
 | 
			
		||||
		exportSelectedFiles,
 | 
			
		||||
		ExportState,
 | 
			
		||||
		exportState
 | 
			
		||||
		exportState,
 | 
			
		||||
		gpxStatistics
 | 
			
		||||
	} from '$lib/stores';
 | 
			
		||||
	import { fileObservers } from '$lib/db';
 | 
			
		||||
	import { Download } from 'lucide-svelte';
 | 
			
		||||
	import {
 | 
			
		||||
		Download,
 | 
			
		||||
		Zap,
 | 
			
		||||
		BrickWall,
 | 
			
		||||
		HeartPulse,
 | 
			
		||||
		Orbit,
 | 
			
		||||
		Thermometer,
 | 
			
		||||
		SquareActivity
 | 
			
		||||
	} from 'lucide-svelte';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import { selection } from './file-list/Selection';
 | 
			
		||||
	import { get } from 'svelte/store';
 | 
			
		||||
	import { GPXStatistics } from 'gpx';
 | 
			
		||||
	import { ListRootItem } from './file-list/FileList';
 | 
			
		||||
 | 
			
		||||
	let open = false;
 | 
			
		||||
	let exportOptions: Record<string, boolean> = {
 | 
			
		||||
		time: true,
 | 
			
		||||
		surface: true,
 | 
			
		||||
		hr: true,
 | 
			
		||||
		cad: true,
 | 
			
		||||
		atemp: true,
 | 
			
		||||
		power: true
 | 
			
		||||
	};
 | 
			
		||||
	let hide: Record<string, boolean> = {
 | 
			
		||||
		time: false,
 | 
			
		||||
		surface: false,
 | 
			
		||||
		hr: false,
 | 
			
		||||
		cad: false,
 | 
			
		||||
		atemp: false,
 | 
			
		||||
		power: false
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	$: if ($exportState !== ExportState.NONE) {
 | 
			
		||||
		open = true;
 | 
			
		||||
		$currentTool = null;
 | 
			
		||||
 | 
			
		||||
		let statistics = $gpxStatistics;
 | 
			
		||||
		if ($exportState === ExportState.ALL) {
 | 
			
		||||
			statistics = Array.from($fileObservers.values())
 | 
			
		||||
				.map((file) => get(file)?.statistics)
 | 
			
		||||
				.reduce((acc, cur) => {
 | 
			
		||||
					if (cur !== undefined) {
 | 
			
		||||
						acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
 | 
			
		||||
					}
 | 
			
		||||
					return acc;
 | 
			
		||||
				}, new GPXStatistics());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		hide.time = statistics.global.time.total === 0;
 | 
			
		||||
		hide.hr = statistics.global.hr.count === 0;
 | 
			
		||||
		hide.cad = statistics.global.cad.count === 0;
 | 
			
		||||
		hide.atemp = statistics.global.atemp.count === 0;
 | 
			
		||||
		hide.power = statistics.global.power.count === 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Dialog.Root
 | 
			
		||||
@@ -32,9 +83,11 @@
 | 
			
		||||
	<Dialog.Trigger class="hidden" />
 | 
			
		||||
	<Dialog.Portal>
 | 
			
		||||
		<Dialog.Content
 | 
			
		||||
			class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-2 border bg-background p-3 shadow-lg rounded-md"
 | 
			
		||||
			class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
 | 
			
		||||
		>
 | 
			
		||||
			<div class="flex flex-row items-center gap-4 border rounded-md p-2">
 | 
			
		||||
			<div
 | 
			
		||||
				class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-accent"
 | 
			
		||||
			>
 | 
			
		||||
				<span>⚠️</span>
 | 
			
		||||
				<span class="max-w-96 text-sm">
 | 
			
		||||
					{$_('menu.support_message')}
 | 
			
		||||
@@ -50,9 +103,9 @@
 | 
			
		||||
					class="grow"
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						if ($exportState === ExportState.SELECTION) {
 | 
			
		||||
							exportSelectedFiles();
 | 
			
		||||
							exportSelectedFiles(exclude);
 | 
			
		||||
						} else if ($exportState === ExportState.ALL) {
 | 
			
		||||
							exportAllFiles();
 | 
			
		||||
							exportAllFiles(exclude);
 | 
			
		||||
						}
 | 
			
		||||
						open = false;
 | 
			
		||||
						$exportState = ExportState.NONE;
 | 
			
		||||
@@ -66,6 +119,63 @@
 | 
			
		||||
					{/if}
 | 
			
		||||
				</Button>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="w-full max-w-xl flex flex-col items-center gap-2">
 | 
			
		||||
				<div class="w-full flex flex-row items-center gap-3">
 | 
			
		||||
					<div class="grow">
 | 
			
		||||
						<Separator />
 | 
			
		||||
					</div>
 | 
			
		||||
					<Label class="shrink-0">
 | 
			
		||||
						{$_('menu.export_options')}
 | 
			
		||||
					</Label>
 | 
			
		||||
					<div class="grow">
 | 
			
		||||
						<Separator />
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
 | 
			
		||||
					<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
 | 
			
		||||
						<Checkbox id="export-time" bind:checked={exportOptions.time} />
 | 
			
		||||
						<Label for="export-time" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<Zap size="16" />
 | 
			
		||||
							{$_('quantities.time')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-1.5">
 | 
			
		||||
						<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
 | 
			
		||||
						<Label for="export-surface" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<BrickWall size="16" />
 | 
			
		||||
							{$_('quantities.surface')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
 | 
			
		||||
						<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
 | 
			
		||||
						<Label for="export-heartrate" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<HeartPulse size="16" />
 | 
			
		||||
							{$_('quantities.heartrate')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
 | 
			
		||||
						<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
 | 
			
		||||
						<Label for="export-cadence" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<Orbit size="16" />
 | 
			
		||||
							{$_('quantities.cadence')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
 | 
			
		||||
						<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
 | 
			
		||||
						<Label for="export-temperature" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<Thermometer size="16" />
 | 
			
		||||
							{$_('quantities.temperature')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
 | 
			
		||||
						<Checkbox id="export-power" bind:checked={exportOptions.power} />
 | 
			
		||||
						<Label for="export-power" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<SquareActivity size="16" />
 | 
			
		||||
							{$_('quantities.power')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</Dialog.Content>
 | 
			
		||||
	</Dialog.Portal>
 | 
			
		||||
</Dialog.Root>
 | 
			
		||||
 
 | 
			
		||||
@@ -320,30 +320,30 @@ export function updateSelectionFromKey(down: boolean, shift: boolean) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function exportFiles(fileIds: string[]) {
 | 
			
		||||
async function exportFiles(fileIds: string[], exclude: string[]) {
 | 
			
		||||
    for (let fileId of fileIds) {
 | 
			
		||||
        let file = getFile(fileId);
 | 
			
		||||
        if (file) {
 | 
			
		||||
            exportFile(file);
 | 
			
		||||
            exportFile(file, exclude);
 | 
			
		||||
            await new Promise(resolve => setTimeout(resolve, 200));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportSelectedFiles() {
 | 
			
		||||
export function exportSelectedFiles(exclude: string[]) {
 | 
			
		||||
    let fileIds: string[] = [];
 | 
			
		||||
    applyToOrderedSelectedItemsFromFile(async (fileId, level, items) => {
 | 
			
		||||
        fileIds.push(fileId);
 | 
			
		||||
    });
 | 
			
		||||
    exportFiles(fileIds);
 | 
			
		||||
    exportFiles(fileIds, exclude);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportAllFiles() {
 | 
			
		||||
    exportFiles(get(fileOrder));
 | 
			
		||||
export function exportAllFiles(exclude: string[]) {
 | 
			
		||||
    exportFiles(get(fileOrder), exclude);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportFile(file: GPXFile) {
 | 
			
		||||
    let blob = new Blob([buildGPX(file)], { type: 'application/gpx+xml' });
 | 
			
		||||
export function exportFile(file: GPXFile, exclude: string[]) {
 | 
			
		||||
    let blob = new Blob([buildGPX(file, exclude)], { type: 'application/gpx+xml' });
 | 
			
		||||
    let url = URL.createObjectURL(blob);
 | 
			
		||||
    let a = document.createElement('a');
 | 
			
		||||
    a.href = url;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@
 | 
			
		||||
        "cut": "Cut",
 | 
			
		||||
        "export": "Export...",
 | 
			
		||||
        "export_all": "Export all...",
 | 
			
		||||
        "export_options": "Export options",
 | 
			
		||||
        "support_message": "The tool is free to use, but not free to run. Please consider supporting the website if you use it frequently. Thank you!",
 | 
			
		||||
        "support_button": "Help keep the website free",
 | 
			
		||||
        "download_file": "Download file",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user