2021-10-28 13:23:29 +00:00
< html >
< head >
< link rel = "stylesheet" href = "../../css/spectre.min.css" >
< / head >
< body >
< div id = "tracks" > < / div >
2022-02-12 18:41:56 +00:00
< div class = "container" id = "toastcontainer" stlye = "position:fixed; bottom:8px; left:0px; right:0px; z-index: 100;" > < / div >
2021-10-28 13:23:29 +00:00
< script src = "../../core/lib/interface.js" > < / script >
2022-02-12 17:49:23 +00:00
< script src = "../../core/js/ui.js" > < / script >
2022-02-12 18:01:59 +00:00
< script src = "../../core/js/utils.js" > < / script >
2021-10-28 13:23:29 +00:00
< script >
var domTracks = document.getElementById("tracks");
2022-07-14 16:10:59 +00:00
function filterGPSCoordinates(track) {
2022-02-07 10:36:28 +00:00
// only include data points with GPS values
2022-07-14 16:10:59 +00:00
var allowNoGPS = localStorage.getItem("recorder-allow-no-gps")=="true";
if (!allowNoGPS)
return track.filter(pt=>pt.Latitude!="" & & pt.Longitude!="");
return track.map(pt => {
if (!isFinite(parseFloat(pt.Latitude))) pt.Latitude=0;
if (!isFinite(parseFloat(pt.Longitude))) pt.Longitude=0;
if (!isFinite(parseFloat(pt.Altitude))) pt.Altitude=0;
return pt;
})
}
function saveKML(track,title) {
// filter out coords with no GPS (or allow, but force to 0)
track = filterGPSCoordinates(track);
2022-02-07 10:36:28 +00:00
// Now output KML
2021-10-28 13:23:29 +00:00
var kml = `<?xml version="1.0" encoding="UTF-8"?>
2023-08-13 18:47:33 +00:00
< kml xmlns = "http://www.opengis.net/kml/2.2" xmlns:gx = "http://www.google.com/kml/ext/2.2" >
2021-10-28 13:23:29 +00:00
< Document >
2021-11-02 20:06:27 +00:00
< Schema id = "schema" >
${track[0].Heartrate!==undefined ? `< gx:SimpleArrayField name = "heartrate" type = "int" >
< displayName > Heart Rate< / displayName >
2022-01-04 16:51:01 +00:00
< / gx:SimpleArrayField > `:``}
2021-11-02 20:06:27 +00:00
${track[0].Steps!==undefined ? `< gx:SimpleArrayField name = "steps" type = "int" >
2021-12-28 21:55:35 +00:00
< displayName > Step Count< / displayName >
< / gx:SimpleArrayField > `:``}
${track[0].Core!==undefined ? `< gx:SimpleArrayField name = "core" type = "int" >
2021-12-15 22:11:18 +00:00
< displayName > Core Temp< / displayName >
2021-12-28 21:55:35 +00:00
< / gx:SimpleArrayField > `:``}
${track[0].Skin!==undefined ? `< gx:SimpleArrayField name = "skin" type = "int" >
< displayName > Skin Temp< / displayName >
2022-01-04 16:51:01 +00:00
< / gx:SimpleArrayField > `:``}
2021-12-28 21:55:35 +00:00
2021-11-02 20:06:27 +00:00
< / Schema >
< Folder >
< name > Tracks< / name >
< Placemark >
< name > ${title}< / name >
< gx:Track >
${track.map(pt=>` < when > ${pt.Time.toISOString()}< / when > \n`).join("")}
${track.map(pt=>` < gx:coord > ${pt.Longitude} ${pt.Latitude} ${pt.Altitude}< / gx:coord > \n`).join("")}
< ExtendedData >
< SchemaData schemaUrl = "#schema" >
${track[0].Heartrate!==undefined ? `< gx:SimpleArrayData name = "heartrate" >
${track.map(pt=>` < gx:value > ${0|pt.Heartrate}< / gx:value > \n`).join("")}
< / gx:SimpleArrayData > `:``}
${track[0].Steps!==undefined ? `< gx:SimpleArrayData name = "steps" >
${track.map(pt=>` < gx:value > ${0|pt.Steps}< / gx:value > \n`).join("")}
< / gx:SimpleArrayData > `:``}
2021-12-28 21:55:35 +00:00
${track[0].Core!==undefined ? `< gx:SimpleArrayData name = "core" >
2021-12-15 22:11:18 +00:00
${track.map(pt=>` < gx:value > ${0|pt.Core}< / gx:value > \n`).join("")}
< / gx:SimpleArrayData > `:``}
2021-12-28 21:55:35 +00:00
${track[0].Skin!==undefined ? `< gx:SimpleArrayData name = "skin" >
${track.map(pt=>` < gx:value > ${0|pt.Skin}< / gx:value > \n`).join("")}
2022-01-04 16:51:01 +00:00
< / gx:SimpleArrayData > `:``}
2021-11-02 20:06:27 +00:00
< / SchemaData >
< / ExtendedData >
< / gx:Track >
< / Placemark >
< / Folder >
2021-10-28 13:23:29 +00:00
< / Document >
< / kml > `;
2023-09-14 11:43:25 +00:00
Util.saveFile(title+".kml", "application/vnd.google-earth.kml+xml", kml);
2022-02-12 18:41:56 +00:00
showToast("Download finished.", "success");
2021-10-28 13:23:29 +00:00
}
function saveGPX(track, title) {
2022-02-12 17:58:15 +00:00
if (!track || !track[0] || !"Time" in track[0] || !track[0].Time) {
2022-02-12 18:41:56 +00:00
showToast("Error in trackfile.", "error");
2022-02-12 17:27:06 +00:00
return;
}
2022-07-14 16:10:59 +00:00
// filter out coords with no GPS (or allow, but force to 0)
track = filterGPSCoordinates(track);
// Output GPX
2021-10-28 13:23:29 +00:00
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
2022-01-04 16:51:01 +00:00
< gpx creator = "Bangle.js" 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 http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx = "http://www.garmin.com/xmlschemas/GpxExtensions/v3" >
2021-10-28 13:23:29 +00:00
< metadata >
2021-11-10 14:55:08 +00:00
< time > ${track[0].Time.toISOString()}< / time >
2021-10-28 13:23:29 +00:00
< / metadata >
< trk >
< name > ${title}< / name >
< trkseg > `;
2023-11-02 02:36:21 +00:00
let lastTime = 0;
2021-10-28 13:23:29 +00:00
track.forEach(pt=>{
2023-11-02 02:36:21 +00:00
let cadence;
2023-11-02 03:05:37 +00:00
if (pt.Steps & & lastTime != 0){
2023-11-02 04:04:09 +00:00
cadence = pt.Steps * 60000 / (pt.Time.getTime() - lastTime);
2023-11-02 04:22:39 +00:00
cadence = cadence / 2; /*Convert from rpm to spm (one cycle is two steps), see https://github.com/espruino/BangleApps/pull/3068#issuecomment-1790041058*/
2023-11-02 02:36:21 +00:00
}
2023-11-02 04:04:09 +00:00
lastTime = pt.Time.getTime();
2023-11-02 02:36:21 +00:00
2022-07-14 16:10:59 +00:00
gpx += `
2021-11-02 20:06:27 +00:00
< trkpt lat = "${pt.Latitude}" lon = "${pt.Longitude}" >
< ele > ${pt.Altitude}< / ele >
< time > ${pt.Time.toISOString()}< / time >
< extensions >
< gpxtpx:TrackPointExtension >
2023-11-02 02:36:21 +00:00
${pt.Heartrate ? `< gpxtpx:hr > ${pt.Heartrate}< / gpxtpx:hr > `:``}
2023-11-02 03:05:37 +00:00
${cadence ? `< gpxtpx:cad > ${cadence}< / gpxtpx:cad > `:``} ${""/*< gpxtpx:distance > ...< / gpxtpx:distance > < gpxtpx:cad > 65< / gpxtpx:cad > */}
2021-11-02 20:06:27 +00:00
< / gpxtpx:TrackPointExtension >
< / extensions >
2021-10-28 13:23:29 +00:00
< / trkpt > `;
2023-11-02 02:36:21 +00:00
2021-10-28 13:23:29 +00:00
});
2021-11-02 20:06:27 +00:00
// https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd
2021-10-28 13:23:29 +00:00
gpx += `
< / trkseg >
< / trk >
< / gpx > `;
2023-09-14 11:43:25 +00:00
Util.saveFile(title+".gpx", "application/gpx+xml", gpx);
2022-02-12 18:41:56 +00:00
showToast("Download finished.", "success");
2021-10-28 13:23:29 +00:00
}
2021-11-02 20:06:27 +00:00
function saveCSV(track, title) {
var headers = Object.keys(track[0]);
var csv = headers.join(",")+"\n";
track.forEach(t=>{
csv += headers.map(k=>{
if (t[k] instanceof Date) return t[k].toISOString();
return t[k];
}).join(",")+"\n";
});
Util.saveCSV(title, csv);
2022-02-12 18:41:56 +00:00
showToast("Download finished.", "success");
2021-11-02 20:06:27 +00:00
}
function trackLineToObject(headers, l) {
2022-02-07 10:36:28 +00:00
if (l===undefined) return {};
2021-10-28 13:23:29 +00:00
var t = l.trim().split(",");
2021-11-02 20:06:27 +00:00
var o = {};
headers.forEach((header,i) => o[header] = t[i]);
if (o.Time) o.Time = new Date(o.Time*1000);
2021-10-28 13:23:29 +00:00
return o;
}
2021-11-02 20:06:27 +00:00
function downloadTrack(filename, callback) {
2021-10-28 13:23:29 +00:00
Util.showModal("Downloading Track...");
2021-11-02 20:06:27 +00:00
Util.readStorageFile(filename,data=>{
2021-10-28 13:23:29 +00:00
Util.hideModal();
2021-11-02 20:06:27 +00:00
var lines = data.trim().split("\n");
var headers = lines.shift().split(",");
var track = lines.map(l=>trackLineToObject(headers, l));
2021-10-28 13:23:29 +00:00
callback(track);
});
}
2021-11-02 20:06:27 +00:00
2021-10-28 13:23:29 +00:00
function getTrackList() {
2021-11-02 20:06:27 +00:00
Util.showModal("Loading Track List...");
2021-10-28 13:23:29 +00:00
domTracks.innerHTML = "";
2021-11-02 20:06:27 +00:00
Puck.eval(`require("Storage").list(/^recorder\\.log.*\\.csv$/,{sf:1})`,files=>{
var trackList = [];
var promise = Promise.resolve();
/* For each file ask Bangle.js for info. Since we now start recording even
before we have a GPS trace, we get the Bangle to do a *quick* search for us
to see if it found any data first. */
files.forEach(filename => {
promise = promise.then(()=>new Promise(resolve => {
var trackNo = filename.match(/^recorder\.log(.*)\.csv$/)[1];
Util.showModal(`Loading Track ${trackNo}...`);
Puck.eval(`(function(fn) {
var f = require("Storage").open(fn,"r");
2022-02-07 10:36:28 +00:00
var headers = f.readLine().trim();
2021-11-02 20:06:27 +00:00
var data = f.readLine();
var lIdx = headers.split(",").indexOf("Latitude");
if (lIdx >= 0) {
var tries = 100;
var l = data;
while (l & & l.split(",")[lIdx]=="" & & tries++)
l = f.readLine();
if (l) data = l;
}
return {headers:headers,l:data};
})(${JSON.stringify(filename)})`, trackInfo=>{
console.log(filename," => ",trackInfo);
2022-02-12 17:27:06 +00:00
if (!trackInfo || !"headers" in trackInfo) {
2022-02-12 18:41:56 +00:00
showToast("Error loading track list.", "error");
2022-02-12 17:27:06 +00:00
resolve();
}
2021-11-02 20:06:27 +00:00
trackInfo.headers = trackInfo.headers.split(",");
trackList.push({
filename : filename,
number : trackNo,
info : trackInfo
});
resolve();
});
}));
2021-10-28 13:23:29 +00:00
});
2021-11-02 20:06:27 +00:00
// ================================================
// When 'promise' completes we now have all the info in trackList
promise.then(() => {
2022-07-14 16:10:59 +00:00
var html = `
< div class = "container" >
< h2 > Tracks< / h2 >
2021-11-02 20:06:27 +00:00
< div class = "columns" > \n`;
trackList.forEach(track => {
console.log("track", track);
2022-02-07 10:36:28 +00:00
var trackData = trackLineToObject(track.info.headers, track.info.l);
2021-11-02 20:06:27 +00:00
console.log("trackData", trackData);
html += `
< div class = "column col-12" >
< div class = "card-header" >
< div class = "card-title h5" > Track ${track.number}< / div >
2022-02-07 10:36:28 +00:00
< div class = "card-subtitle text-gray" > ${trackData.Time?trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }):"No track data"}< / div >
2021-11-02 20:06:27 +00:00
< / div >
${trackData.Latitude ? `
< div class = "card-image" >
< iframe
width="100%"
height="250"
frameborder="0" style="border:0"
2021-11-10 14:55:08 +00:00
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBxTcwrrVOh2piz7EmIs1Xn4FsRxJWeVH4& q=${trackData.Latitude},${trackData.Longitude}& zoom=10" allowfullscreen>
2021-11-02 20:06:27 +00:00
< / iframe >
< / div > < div class = "card-body" > < / div > ` : `< div class = "card-body" > No GPS info< / div > `}
< div class = "card-footer" >
< button class = "btn btn-primary" filename = "${track.filename}" trackid = "${track.number}" task = "downloadkml" > Download KML< / button >
< button class = "btn btn-primary" filename = "${track.filename}" trackid = "${track.number}" task = "downloadgpx" > Download GPX< / button >
< button class = "btn btn-primary" filename = "${track.filename}" trackid = "${track.number}" task = "downloadcsv" > Download CSV< / button >
< button class = "btn btn-default" filename = "${track.filename}" trackid = "${track.number}" task = "delete" > Delete< / button >
< / div >
< / div >
2021-10-28 13:23:29 +00:00
`;
2021-11-02 20:06:27 +00:00
});
if (trackList.length==0) {
html += `
< div class = "column col-12" >
< div class = "card-header" >
< div class = "card-title h5" > No tracks< / div >
< div class = "card-subtitle text-gray" > No GPS tracks found< / div >
< / div >
< / div >
`;
2021-10-28 13:23:29 +00:00
}
2021-11-02 20:06:27 +00:00
html += `
2022-07-14 16:10:59 +00:00
< / div > <!-- columns -->
< h2 > Settings< / h2 >
< div class = "form-group" >
< label class = "form-switch" >
< input type = "checkbox" id = "settings-allow-no-gps" $ { ( localStorage . getItem ( " recorder-allow-no-gps " ) = = " true " ) ? " checked " : " " } >
< i class = "form-icon" > < / i > Include GPX/KML entries even when there's no GPS info
< / label >
2021-11-02 20:06:27 +00:00
< / div >
< / div > `;
domTracks.innerHTML = html;
2022-07-14 16:10:59 +00:00
document.getElementById("settings-allow-no-gps").addEventListener("change",event=>{
var allowNoGPS = event.target.checked;
localStorage.setItem("recorder-allow-no-gps", allowNoGPS);
});
2021-11-02 20:06:27 +00:00
Util.hideModal();
var buttons = domTracks.querySelectorAll("button");
for (var i=0;i< buttons.length ; i + + ) {
buttons[i].addEventListener("click",event => {
var button = event.currentTarget;
var filename = button.getAttribute("filename");
var trackid = parseInt(button.getAttribute("trackid"));
if (!filename || trackid===undefined) return;
var task = button.getAttribute("task");
if (task=="delete") {
Util.showModal("Deleting Track...");
Util.eraseStorageFile(filename,()=>{
Util.hideModal();
getTrackList();
});
}
if (task=="downloadkml") {
downloadTrack(filename, track => saveKML(track, `Bangle.js Track ${trackid}`));
}
if (task=="downloadgpx") {
downloadTrack(filename, track => saveGPX(track, `Bangle.js Track ${trackid}`));
}
if (task=="downloadcsv") {
downloadTrack(filename, track => saveCSV(track, `Bangle.js Track ${trackid}`));
}
});
}
});
});
2021-10-28 13:23:29 +00:00
}
function onInit() {
getTrackList();
}
< / script >
< / body >
< / html >