Created
January 19, 2022 16:26
-
-
Save mcattarinussi/3f0791514bad89dc39f63688abb3d5ca to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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