Skip to content

Instantly share code, notes, and snippets.

@mcattarinussi
Created January 19, 2022 16:26
Show Gist options
  • Save mcattarinussi/3f0791514bad89dc39f63688abb3d5ca to your computer and use it in GitHub Desktop.
Save mcattarinussi/3f0791514bad89dc39f63688abb3d5ca to your computer and use it in GitHub Desktop.
/*
The gpx dowloaded from the activity page on tabaccomapp website does not include <time> elements inside the <trkpt>,
this prevents to upload a gpx downloaded from tabaccomapp to Strava.
This script uses the tabaccomapp json api to construct a gpx containing time elements.
Install dependencies: npm i axios ramda xml2js
Usage: node index.js <TABACCOMAPP-ACTIVITY-URL> <ACTIVITY-START-DATE-ISO> > output.gpx
Example: node index.js https://tabaccomapp-community.it/en/percorso/123-xxx-yyy 2022-01-01T00:00:00.000Z > 123-xxx-yyy.gpx
*/
const R = require('ramda');
const axios = require('axios');
const xml2js = require('xml2js');
const tempo2Milliseconds = R.pipe(
R.split(':'),
R.map(Number),
([ hours, minutes ]) => (hours * 60 + minutes) * 60 * 1000,
);
const parseActivityTitleFromUrl = R.pipe(
u => u.replaceAll('/', ' ').trim().split(' '),
R.last,
u => u.split('-').slice(1).join(' '),
s => s.charAt(0).toUpperCase() + s.slice(1)
);
(async () => {
const [, , activityUrl, activityStartIsoDate, ..._] = process.argv;
const activityStartDate = new Date(activityStartIsoDate);
const activityStartMs = Math.round(activityStartDate.getTime() / 1000) * 1000 ;
const { data: { percorso } } = await axios.get(activityUrl + '/json/', { headers: {
Accept: 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest'
}});
// The `tempo` property of each `percorso` item is a time string following the current format hh:mm. It represents
// the number of hours and minutes elapsed since the start of the activity and has granularity of 1 minute.
// Here we are grouping items having the same `tempo` and we try to assign a more precise datetetime:
// the granularity of each "minute" is calculated based on how many items with that `tempo` we have.
const trkptDateTimes = R.pipe(
R.pluck('tempo'),
R.map(tempo2Milliseconds),
R.groupWith(R.equals),
R.map(elements => elements.map(
(timeMs, idx) => new Date(
activityStartMs +
timeMs +
Math.round(60 / elements.length * idx * 100) / 100 * 1000
)
)),
R.flatten,
R.sort(R.ascend),
)(percorso);
const gpx = {
gpx: {
'$': {
version: '1.1',
xmlns: 'http://www.topografix.com/GPX/1/1',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation': 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
},
metadata: {
name: parseActivityTitleFromUrl(activityUrl),
time: activityStartDate.toISOString()
},
trk: {
number: 1,
trkseg: [
{
trkpt: percorso.map(({ altitudine: ele, latitude: lat, longitude: lon }, idx) => ({
$: { lat, lon },
ele,
time: trkptDateTimes[idx].toISOString()
}))
}
]
}
}
}
console.log(new xml2js.Builder().buildObject(gpx));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment