Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPX file support. Use attributes for magic numbers. #2

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions example/gpx_example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const authenticate = require('endomondo-unofficial-api').authenticate;
const workouts = require('endomondo-unofficial-api').workouts;
const workoutGet = require('endomondo-unofficial-api').workout.get;
const workoutSet = require('endomondo-unofficial-api').workout.set
const fixTracks = require('endomondo-unofficial-api').workout.fix
const track = require('endomondo-unofficial-api').attributes.track;
const sport = require('endomondo-unofficial-api').attributes.sport;
const gpx = require('endomondo-unofficial-api').gpx;

/* example of a valid track point set */
const trackPoints = [
{
time: Date.parse("2017-05-12 15:39:25 UTC"),
longitude: 8.2555906,
latitude: 47.3491986,
altitude: 444.1,
inst: track.START
},
{
time: Date.parse("2017-05-12 15:39:27 UTC"),
longitude: 8.1555906,
latitude: 47.2491986,
altitude: 444.1,
},
{
time: Date.parse("2017-05-12 15:39:30 UTC"),
longitude: 8.1665917,
latitude: 47.3492244,
altitude: 444.1,
inst: track.END
}
];

const main = async() => {

/* read tracks from gpx file */
let tracks = await new Promise((resolve) => gpx.parseGpxFromFile("Current.gpx", (err, data) => {
if(err) {
console.log("Could not parse GPX file");
reject(err);
}
resolve(data);
}));

/* connect to endomondo */
const auth = await authenticate({email: "<username>", password: "<password>"});
console.log(auth);

/* fix / split tracks if long delay between two points */
tracks = fixTracks(tracks, 24 * 60, 0.1);
console.log("Numbor of tracks: " + tracks.length);
//return;

for(let i = 0; i < tracks.length; i++) {
const track = tracks[i];

/* create new workout, write first couple of points */
const step = 20; // write 20 points in one request
const newWork = await workoutSet({
authToken: auth.authToken,
sport:sport.CYCLING,
distance: 0,
duration: 0,
points: track.slice(0, step)
});
//console.log(newWork);

/* write remaining points to existing workout */
for(let idx = 0; idx < track.length; idx += step)
{
const ps = track.slice(idx, idx + step);
const updWork = await workoutSet({
authToken: auth.authToken,
userId: auth.userId,
workoutId: newWork.workoutId,
sport: sport.CYCLING,
distance: ps[ps.length - 1].dist.all,
duration: (ps[ps.length - 1].time - track[0].time) / 1000,
points: ps
});
console.log(idx + "-" + (idx + ps.length) + ": " + JSON.stringify(ps[ps.length - 1]));

/* wait befor sending next points */
await new Promise((resolve) => setTimeout(resolve, 100));
}
}

/* get a list of all workouts */
const works = await workouts({authToken: auth.authToken});
console.log(works);

/* show latest workout */
const work = await workoutGet({authToken: auth.authToken, workoutId: works.data[0].id});
console.log(work);

};
main();
10 changes: 7 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

const authenticate = require('./lib/authentication'),
workouts = require('./lib/workouts'),
workout = require('./lib/workout')
workout = require('./lib/workout'),
attributes = require('./lib/attributes'),
gpx = require('./lib/gpx')

module.exports= {
authenticate: authenticate,
workouts: workouts,
workout: workout
}
workout: workout,
attributes: attributes,
gpx: gpx
}
18 changes: 18 additions & 0 deletions lib/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* Track commands are used to indicate current tracking state for tracking points. */
var track = {
START: 2,
PAUSE: 0,
RESTART: 1,
END: 3
};

/* Sport codes */
var sport = {
RUNNING: 0,
CYCLING: 2
};

module.exports = {
track: track,
sport: sport
}
79 changes: 79 additions & 0 deletions lib/gpx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict'

var gpxParse = require("gpx-parse");
var attributes = require('./attributes'),
sportAttr = attributes.sport,
trackAttr = attributes.track

var EARTH_RADIUS_KM = 6371.0;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains quite a bit of logic, that i am sure its already done by some package (i think i have used in a different project. If you want we can talk about it or i can look at it to refactor on the future

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok for me to not include this file. There might be packages to get the distance of two coordinates.


function deg2rad(deg) {
return (deg * Math.PI / 180);
}

function rad2deg(rad) {
return (rad * 180 / Math.PI);
}

function distanceEarth(lat1, lon1, lat2, lon2) {
var lat1r, lon1r, lat2r, lon2r, u, v;
lat1r = deg2rad(lat1);
lon1r = deg2rad(lon1);
lat2r = deg2rad(lat2);
lon2r = deg2rad(lon2);
u = Math.sin((lat2r - lat1r)/2);
v = Math.sin((lon2r - lon1r)/2);
return 2.0 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(u * u + Math.cos(lat1r) * Math.cos(lat2r) * v * v));
};

function parseGpxFromFile(file, cb) {
new Promise((resolve, reject) => {
gpxParse.parseGpxFromFile(file, function(error, data) {
if(error) {
reject(error);
}
var tracks = data.tracks.map(function(track) {
var points = [];
track.segments.map(function(segment) {
var _dist = 0;
var last = segment[0];
var ps = segment.map((s) => {
var l = last;
last = s;
var dist = distanceEarth(l.lat, l.lon, s.lat, s.lon);
_dist += dist;
return {
time: Date.parse(s.time),
longitude: s.lon,
latitude: s.lat,
altitude: s.elevation,
dist: {last: dist, all: _dist}, // km
speed: (dist * 1000 / ((Date.parse(s.time) - Date.parse(l.time)) / 1000)) // m/s
}
});
if(ps.length > 1) {
ps[0].inst = trackAttr.RESTART; //mark first point as restart
}
if(ps.length > 0) {
ps[ps.length - 1].inst = trackAttr.PAUSE; //mark last point as pause
}
points = points.concat(ps);
});
if(points.length > 1) {
points[0].inst = trackAttr.START; //mark first point as start
}
if(points.length > 0) {
points[points.length - 1].inst = trackAttr.END; //mark last point as end
}
return points;
});
resolve(tracks);
});
}).then((tracks) => cb(null, tracks), function(error) {
cb(error);
});
}

module.exports = {
parseGpxFromFile: parseGpxFromFile
}
89 changes: 75 additions & 14 deletions lib/workout.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ var request = require('request'),
apiUrl = common.urls.api,
paths = common.urls.paths,
regex = common.regex.workout,
handleError = common.handleError
handleError = common.handleError,
attributes = require('./attributes'),
sportAttr = attributes.sport,
trackAttr = attributes.track

function getQueryString(params){
return {
Expand All @@ -21,7 +24,7 @@ function getQueryString(params){
}

function workoutGet(params) {
return new Promise((resolve, reject) =>{
return new Promise((resolve, reject) =>{
var qs = getQueryString(params)
var url = apiUrl + paths.activity.get

Expand All @@ -37,7 +40,7 @@ function workoutGet(params) {
function getWorkoutParams(params){
return{
workoutId: params.id,
sport: params.sport || 0,
sport: params.sport || sportAttr.RUNNING,
duration: params.duration || 0,
gzip: params.gzip || true,
audioMessage: params.audioMessage || false,
Expand All @@ -49,45 +52,103 @@ function getWorkoutParams(params){
}

function convertWorkout(workoutPoints){
var points = ''
var points = '';
for(var workoutPoint of workoutPoints){
points += moment.utc(workoutPoint.time).format('YYYY-MM-DD hh:mm:ss UTC') + ';' +
workoutPoint.inst + ';' +
workoutPoint.latitude + ';' +
workoutPoint.longitude + ';' +
workoutPoint.distance.toFixed(2) + ';' +
workoutPoint.speed + ';'
workoutPoint.altitude + ';' +
workoutPoint.heartRate + '\n'
(workoutPoint.latitude ? workoutPoint.latitude.toFixed(8) : '') + ';' +
(workoutPoint.longitude ? workoutPoint.longitude.toFixed(8) : '') + ';' +
(workoutPoint.distance ? workoutPoint.distance.toFixed(3) : '') + ';' +
(workoutPoint.speed ? workoutPoint.speed.toFixed(3) : '') + ';' +
(workoutPoint.altitude ? workoutPoint.altitude.toFixed(3) : '') + ';' +
workoutPoint.heartRate + ';\n'
}
return points.replace(/undefined/g,'');
}

function postQueryString(params){
var points = params.points ? params.points : [{time: params.time, distance: params.distance, inst: 3}]
var points = params.points ? params.points : [{time: params.time, distance: params.distance, inst: trackAttr.END}]
return {
authToken: params.authToken,
duration: params.duration,
distance: params.distance || 0,
sport: params.sport || sportAttr.RUNNING,
trackPoints: convertWorkout(points),
workoutId: params.workoutId || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
}
}

function workoutSet(params) {
return new Promise((resolve, reject) =>{
return new Promise((resolve, reject) =>{
var qs = postQueryString(params)
var url = apiUrl + paths.activity.post

request.post({ url: url, qs: qs }, (err, response, body) => {
if (err || !regex.isOk.test(body))
{
console.log(body);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this debug line should be removed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, have removed it in my branch.

reject(handleError(err))
}
else
resolve({workoutId: qs.workoutId})
});
})
}

function fixTracks(tracks, splitAfterMin, pauseBelowSpeed) {
var _setInst = function(t) {
if(t.length > 1) {
t[0].inst = trackAttr.START; //mark first point as start
}
if(t.length > 0) {
t[t.length - 1].inst = trackAttr.END; //mark last point as end
}
return t;
};

var splitAfter = splitAfterMin ? (splitAfterMin * 60 * 1000) : (24 * 60 * 60 * 1000);
var pauseSpeed = pauseBelowSpeed || 0.03;
var pause = false;

var res = [];
tracks.forEach(function(track){
var newTrack = [];
track.forEach(function(p){
/* for a new track add a point */
if(newTrack.length == 0){
newTrack.push(p);
}
/* mark point as paused if below thresholde value, do not add further low speed points */
else if (p.speed < pauseSpeed) {
if(!pause) {
pause = true;
p.inst = trackAttr.PAUSE;
}
}
/* detect new track after long pause, e.g. next day */
else if((p.time - splitAfter) > newTrack[newTrack.length - 1].time) {
res.push(_setInst(newTrack));
newTrack = [];
}
/* add point to current track */
else {
if(pause){
pause = false;
p.inst = trackAttr.RESTART;
}
newTrack.push(p);
}
});
/* add track if no more points available */
if(newTrack.length > 0){
res.push(_setInst(newTrack));
}
});
return res;
}

module.exports = {
get: workoutGet,
set: workoutSet
}
set: workoutSet,
fix: fixTracks
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"dependencies": {
"moment": "^2.17.1",
"request": "^2.79.0",
"uuidv5": "^1.0.0"
"uuidv5": "^1.0.0",
"gpx-parse": "^0.10.4"
}
}