Skip to content

Commit

Permalink
Merge pull request #12 from Its-Just-Nans/add-duration
Browse files Browse the repository at this point in the history
Try to add duration
  • Loading branch information
We-Gold authored Nov 21, 2024
2 parents 347b096 + b49b35c commit 0cd899e
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 26 deletions.
56 changes: 42 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The schema for GPX, a commonly used gps tracking format, can be found here: [GPX

See [Documentation](#documentation) for more details on how GPX data is represented by the library.

# Usage
## Usage

**This library does include support for non-browser execution.**

Expand Down Expand Up @@ -141,7 +141,23 @@ I do try to be responsive to PRs though, so if you leave one I'll try to get it

Also, there are some basic tests built in to the library, so please test your code before you try to get it merged (_just to make sure everything is backwards compatible_). Use `npm run test` to do this.

# Documentation
## Options

You can also run `parseGPX()` with custom options for more control over the parsing process. See [options.ts](./lib/options.ts) for more details.

```js
const [parsedFile, error] = parseGPX(data, {
avgSpeedThreshold: 0.1,
})
// same for parseGPXWithCustomParser()
```

| Property | Type | Description |
| ----------------- | ------- | ----------------------------------------------------------------------- |
| removeEmptyFields | boolean | delete null fields in output |
| avgSpeedThreshold | number | average speed threshold (in m/ms) used to determine the moving duration |

## Types

These descriptions are adapted from [GPXParser.js](https://github.com/Luuka/GPXParser.js), with minor modifications.

Expand All @@ -155,7 +171,7 @@ For specific type definition, see [types.ts](./lib/types.ts).
| tracks | Array of Tracks | Array of waypoints of tracks |
| routes | Array of Routes | Array of waypoints of routes |

## Metadata
### Metadata

| Property | Type | Description |
| ----------- | ----------- | ------------- |
Expand All @@ -165,7 +181,7 @@ For specific type definition, see [types.ts](./lib/types.ts).
| author | Float | Author object |
| time | String | Time |

## Waypoint
### Waypoint

| Property | Type | Description |
| ----------- | ------ | ----------------- |
Expand All @@ -177,7 +193,7 @@ For specific type definition, see [types.ts](./lib/types.ts).
| elevation | Float | Point elevation |
| time | Date | Point time |

## Track
### Track

| Property | Type | Description |
| ----------- | ---------------- | ------------------------------------- |
Expand All @@ -189,11 +205,12 @@ For specific type definition, see [types.ts](./lib/types.ts).
| link | String | Link to a web address |
| type | String | Track type |
| points | Array | Array of Points |
| distance | Distance Object | Distance information about the Route |
| elevation | Elevation Object | Elevation information about the Route |
| distance | Distance Object | Distance information about the Track |
| duration | Duration Object | Duration information about the Track |
| elevation | Elevation Object | Elevation information about the Track |
| slopes | Float Array | Slope of each sub-segment |

## Route
### Route

| Property | Type | Description |
| ----------- | ---------------- | ------------------------------------- |
Expand All @@ -206,10 +223,11 @@ For specific type definition, see [types.ts](./lib/types.ts).
| type | String | Route type |
| points | Array | Array of Points |
| distance | Distance Object | Distance information about the Route |
| duration | Duration Object | Duration information about the Route |
| elevation | Elevation Object | Elevation information about the Route |
| slopes | Float Array | Slope of each sub-segment |

## Point
### Point

| Property | Type | Description |
| --------- | ----- | --------------- |
Expand All @@ -218,14 +236,24 @@ For specific type definition, see [types.ts](./lib/types.ts).
| elevation | Float | Point elevation |
| time | Date | Point time |

## Distance
### Distance

| Property | Type | Description |
| ---------- | ----- | ---------------------------------------------------- |
| total | Float | Total distance of the Route/Track |
| cumulative | Float | Cumulative distance at each point of the Route/Track |

## Elevation
### Duration

| Property | Type | Description |
| -------------- | ------------ | ---------------------------------------------------- |
| cumulative | Float | Cumulative duration at each point of the Route/Track |
| movingDuration | Float | Total moving duration of the Route/Track in seconds |
| totalDuration | Float | Total duration of the Route/Track in seconds |
| startTime | Date or null | Start date, if available |
| endTime | Date or null | End date, if available |

### Elevation

| Property | Type | Description |
| -------- | ----- | ----------------------------- |
Expand All @@ -235,22 +263,22 @@ For specific type definition, see [types.ts](./lib/types.ts).
| negative | Float | Negative elevation difference |
| average | Float | Average elevation |

## Author
### Author

| Property | Type | Description |
| -------- | ------------ | --------------------------- |
| name | String | Author name |
| email | Email object | Email address of the author |
| link | Link object | Web address |

## Email
### Email

| Property | Type | Description |
| -------- | ------ | ------------ |
| id | String | Email id |
| domain | String | Email domain |

## Link
### Link

| Property | Type | Description |
| -------- | ------ | ----------- |
Expand Down
86 changes: 85 additions & 1 deletion lib/math_helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Point, Distance, Elevation } from "./types"
import { Point, Distance, Elevation, Duration, Options } from "./types"

/**
* Calculates the distances along a series of points using the haversine formula internally.
Expand Down Expand Up @@ -49,6 +49,90 @@ export const haversineDistance = (point1: Point, point2: Point): number => {
return 6371000 * c
}

/**
* Calculates duration statistics based on distance traveled and the time taken.
*
*
* @param points A list of points with a time
* @param distance A distance object containing the total distance and the cumulative distances
* @returns A duration object
*/


export const calculateDuration = (
points: Point[],
distance: Distance,
calculOptions: Options
): Duration => {
const { avgSpeedThreshold } = calculOptions
const allTimedPoints: { time: Date; distance: number }[] = []
const cumulative: number[] = [0]
let lastTime = 0

for (let i = 0; i < points.length - 1; i++) {
const currentPoint = points[i]
const time = currentPoint.time
const dist = distance.cumulative[i]
const previousPoint = cumulative[i]

if (time !== null) {
const movingTime = time.getTime() - lastTime

if (movingTime > 0) {
// Calculate average speed over the last 10 seconds
let sumDistances = 0
let sumTime = 0

for (let j = i; j >= 0; j--) {
const prevTime = points[j].time?.getTime()
if (prevTime !== undefined) {
const timeDiff = time.getTime() - prevTime
if (timeDiff > 10000) break // Only include last 10 seconds
sumDistances +=
distance.cumulative[j + 1] - distance.cumulative[j]
sumTime += timeDiff
}
}

const avgSpeed = sumTime > 0 ? sumDistances / sumTime : 0

// Determine if average speed indicates resting
const nextCumul =
avgSpeed > avgSpeedThreshold
? previousPoint + movingTime // Significant movement
: previousPoint // Resting, no time added

cumulative.push(nextCumul)
} else {
// Handle edge case of no movement
cumulative.push(previousPoint)
}

lastTime = time.getTime()
allTimedPoints.push({ time, distance: dist })
} else {
// Missing time, do not contribute to cumulative
cumulative.push(previousPoint)
}
}

const totalDuration =
allTimedPoints.length === 0
? 0
: allTimedPoints[allTimedPoints.length - 1].time.getTime() -
allTimedPoints[0].time.getTime()

return {
startTime: allTimedPoints.length ? allTimedPoints[0].time : null,
endTime: allTimedPoints.length
? allTimedPoints[allTimedPoints.length - 1].time
: null,
cumulative,
movingDuration: cumulative[cumulative.length - 1] / 1000, // Convert to seconds
totalDuration: totalDuration / 1000, // Convert to seconds
}
}

/**
* Calculates details about the elevation of the given points.
* Points without elevations will be skipped.
Expand Down
8 changes: 8 additions & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const DEFAULT_THRESHOLD = 0.000215 // m/ms - 0.215 m/s - 0.774000 km/h

export const DEFAULT_OPTIONS = {
// delete null fields in output
removeEmptyFields: true,
// average speed threshold (in m/ms) used to determine the moving duration
avgSpeedThreshold: DEFAULT_THRESHOLD,
}
45 changes: 36 additions & 9 deletions lib/parse.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
calculateDistance,
calculateDuration,
calculateElevation,
calculateSlopes,
} from "./math_helpers"
import { DEFAULT_OPTIONS } from "./options"
import { ParsedGPX } from "./parsed_gpx"
import {
ParsedGPXInputs,
Expand All @@ -11,6 +13,7 @@ import {
Track,
Waypoint,
Extensions,
Options,
} from "./types"

/**
Expand All @@ -20,16 +23,24 @@ import {
* @param removeEmptyFields Whether or not to remove null or undefined fields from the output
* @returns A ParsedGPX with all of the parsed data and a method to convert to GeoJSON
*/
export const parseGPX = (gpxSource: string, removeEmptyFields: boolean = true) => {
export const parseGPX = (
gpxSource: string,
options: Options = DEFAULT_OPTIONS
) => {
const parseMethod = (gpxSource: string): Document | null => {
// Verify that we are in a browser
if (typeof document == undefined) return null

if (typeof document === "undefined") return null
if (typeof window === "undefined") {
console.error(
"window is undefined, try to use the parseGPXWithCustomParser method"
)
return null
}
const domParser = new window.DOMParser()
return domParser.parseFromString(gpxSource, "text/xml")
}

return parseGPXWithCustomParser(gpxSource, parseMethod, removeEmptyFields)
const allOptions = { ...DEFAULT_OPTIONS, ...options }
return parseGPXWithCustomParser(gpxSource, parseMethod, allOptions)
}

/**
Expand All @@ -44,7 +55,7 @@ export const parseGPX = (gpxSource: string, removeEmptyFields: boolean = true) =
export const parseGPXWithCustomParser = (
gpxSource: string,
parseGPXToXML: (gpxSource: string) => Document | null,
removeEmptyFields: boolean = true
options: Options = DEFAULT_OPTIONS
): [null, Error] | [ParsedGPX, null] => {
// Parse the GPX string using the given parse method
const parsedSource = parseGPXToXML(gpxSource)
Expand Down Expand Up @@ -150,6 +161,13 @@ export const parseGPXWithCustomParser = (
cumulative: [],
total: 0,
},
duration: {
cumulative: [],
movingDuration: 0,
totalDuration: 0,
endTime: null,
startTime: null,
},
elevation: {
maximum: null,
minimum: null,
Expand Down Expand Up @@ -196,6 +214,7 @@ export const parseGPXWithCustomParser = (
}

route.distance = calculateDistance(route.points)
route.duration = calculateDuration(route.points, route.distance, options)
route.elevation = calculateElevation(route.points)
route.slopes = calculateSlopes(route.points, route.distance.cumulative)

Expand All @@ -217,6 +236,13 @@ export const parseGPXWithCustomParser = (
cumulative: [],
total: 0,
},
duration: {
cumulative: [],
movingDuration: 0,
totalDuration: 0,
startTime: null,
endTime: null,
},
elevation: {
maximum: null,
minimum: null,
Expand Down Expand Up @@ -273,20 +299,21 @@ export const parseGPXWithCustomParser = (
}

track.distance = calculateDistance(track.points)
track.duration = calculateDuration(track.points, track.distance, options)
track.elevation = calculateElevation(track.points)
track.slopes = calculateSlopes(track.points, track.distance.cumulative)

output.tracks.push(track)
}

if (removeEmptyFields) {
if (options.removeEmptyFields) {
deleteNullFields(output.metadata)
deleteNullFields(output.waypoints)
deleteNullFields(output.tracks)
deleteNullFields(output.routes)
}

return [new ParsedGPX(output, removeEmptyFields), null]
return [new ParsedGPX(output, options.removeEmptyFields), null]
}

const parseExtensions = (
Expand Down Expand Up @@ -367,7 +394,7 @@ const querySelectDirectDescendant = (
export const deleteNullFields = <T>(object: T) => {
// Return non-object values as-is
if (typeof object !== 'object' || object === null || object === undefined) {
return
return
}

// Remove null fields from arrays
Expand Down
Loading

0 comments on commit 0cd899e

Please sign in to comment.