export options

This commit is contained in:
vcoppe
2024-07-23 11:20:31 +02:00
parent 1be9059e39
commit 75d9813fe0
13 changed files with 258 additions and 4279 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}
}

View File

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

View File

@@ -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,

View File

@@ -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());
}
}
}
});
});

View File

@@ -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);
});
});

View File

@@ -7,7 +7,6 @@
"moduleResolution": "node",
},
"include": [
"src",
"test"
"src"
],
}

View File

@@ -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"
}
},

View File

@@ -67,4 +67,4 @@
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1"
}
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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",