mirror of https://github.com/espruino/BangleApps
302 lines
12 KiB
HTML
302 lines
12 KiB
HTML
<html>
|
|
<head>
|
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
|
</head>
|
|
<body>
|
|
<div id="tracks"></div>
|
|
<div class="container" id="toastcontainer" stlye="position:fixed; bottom:8px; left:0px; right:0px; z-index: 100;"></div>
|
|
|
|
<script src="../../core/lib/interface.js"></script>
|
|
<script src="../../core/js/ui.js"></script>
|
|
<script src="../../core/js/utils.js"></script>
|
|
<script>
|
|
var domTracks = document.getElementById("tracks");
|
|
|
|
function filterGPSCoordinates(track) {
|
|
// only include data points with GPS values
|
|
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);
|
|
// Now output KML
|
|
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
|
|
<Document>
|
|
<Schema id="schema">
|
|
${track[0].Heartrate!==undefined ? `<gx:SimpleArrayField name="heartrate" type="int">
|
|
<displayName>Heart Rate</displayName>
|
|
</gx:SimpleArrayField>`:``}
|
|
${track[0].Steps!==undefined ? `<gx:SimpleArrayField name="steps" type="int">
|
|
<displayName>Step Count</displayName>
|
|
</gx:SimpleArrayField>`:``}
|
|
${track[0].Core!==undefined ? `<gx:SimpleArrayField name="core" type="int">
|
|
<displayName>Core Temp</displayName>
|
|
</gx:SimpleArrayField>`:``}
|
|
${track[0].Skin!==undefined ? `<gx:SimpleArrayField name="skin" type="int">
|
|
<displayName>Skin Temp</displayName>
|
|
</gx:SimpleArrayField>`:``}
|
|
|
|
</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>`:``}
|
|
${track[0].Core!==undefined ? `<gx:SimpleArrayData name="core">
|
|
${track.map(pt=>` <gx:value>${0|pt.Core}</gx:value>\n`).join("")}
|
|
</gx:SimpleArrayData>`:``}
|
|
${track[0].Skin!==undefined ? `<gx:SimpleArrayData name="skin">
|
|
${track.map(pt=>` <gx:value>${0|pt.Skin}</gx:value>\n`).join("")}
|
|
</gx:SimpleArrayData>`:``}
|
|
</SchemaData>
|
|
</ExtendedData>
|
|
</gx:Track>
|
|
</Placemark>
|
|
</Folder>
|
|
</Document>
|
|
</kml>`;
|
|
Util.saveFile(title+".kml", "application/vnd.google-earth.kml+xml", kml);
|
|
showToast("Download finished.", "success");
|
|
}
|
|
|
|
function saveGPX(track, title) {
|
|
if (!track || !track[0] || !"Time" in track[0] || !track[0].Time) {
|
|
showToast("Error in trackfile.", "error");
|
|
return;
|
|
}
|
|
// filter out coords with no GPS (or allow, but force to 0)
|
|
track = filterGPSCoordinates(track);
|
|
// Output GPX
|
|
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<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">
|
|
<metadata>
|
|
<time>${track[0].Time.toISOString()}</time>
|
|
</metadata>
|
|
<trk>
|
|
<name>${title}</name>
|
|
<trkseg>`;
|
|
let lastTime = 0;
|
|
track.forEach(pt=>{
|
|
let cadence;
|
|
if (pt.Steps && lastTime != 0){
|
|
cadence = pt.Steps * 60000 / (pt.Time.getTime() - lastTime);
|
|
cadence = cadence / 2; /*Convert from rpm to spm (one cycle is two steps), see https://github.com/espruino/BangleApps/pull/3068#issuecomment-1790041058*/
|
|
}
|
|
lastTime = pt.Time.getTime();
|
|
|
|
gpx += `
|
|
<trkpt lat="${pt.Latitude}" lon="${pt.Longitude}">
|
|
<ele>${pt.Altitude}</ele>
|
|
<time>${pt.Time.toISOString()}</time>
|
|
<extensions>
|
|
<gpxtpx:TrackPointExtension>
|
|
${pt.Heartrate ? `<gpxtpx:hr>${pt.Heartrate}</gpxtpx:hr>`:``}
|
|
${cadence ? `<gpxtpx:cad>${cadence}</gpxtpx:cad>`:``} ${""/*<gpxtpx:distance>...</gpxtpx:distance><gpxtpx:cad>65</gpxtpx:cad>*/}
|
|
</gpxtpx:TrackPointExtension>
|
|
</extensions>
|
|
</trkpt>`;
|
|
|
|
});
|
|
// https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd
|
|
gpx += `
|
|
</trkseg>
|
|
</trk>
|
|
</gpx>`;
|
|
Util.saveFile(title+".gpx", "application/gpx+xml", gpx);
|
|
showToast("Download finished.", "success");
|
|
}
|
|
|
|
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);
|
|
showToast("Download finished.", "success");
|
|
}
|
|
|
|
function trackLineToObject(headers, l) {
|
|
if (l===undefined) return {};
|
|
var t = l.trim().split(",");
|
|
var o = {};
|
|
headers.forEach((header,i) => o[header] = t[i]);
|
|
if (o.Time) o.Time = new Date(o.Time*1000);
|
|
return o;
|
|
}
|
|
|
|
function downloadTrack(filename, callback) {
|
|
Util.showModal("Downloading Track...");
|
|
Util.readStorageFile(filename,data=>{
|
|
Util.hideModal();
|
|
var lines = data.trim().split("\n");
|
|
var headers = lines.shift().split(",");
|
|
var track = lines.map(l=>trackLineToObject(headers, l));
|
|
callback(track);
|
|
});
|
|
}
|
|
|
|
function getTrackList() {
|
|
Util.showModal("Loading Track List...");
|
|
domTracks.innerHTML = "";
|
|
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");
|
|
var headers = f.readLine().trim();
|
|
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);
|
|
if (!trackInfo || !"headers" in trackInfo) {
|
|
showToast("Error loading track list.", "error");
|
|
resolve();
|
|
}
|
|
trackInfo.headers = trackInfo.headers.split(",");
|
|
trackList.push({
|
|
filename : filename,
|
|
number : trackNo,
|
|
info : trackInfo
|
|
});
|
|
resolve();
|
|
});
|
|
}));
|
|
});
|
|
// ================================================
|
|
// When 'promise' completes we now have all the info in trackList
|
|
promise.then(() => {
|
|
var html = `
|
|
<div class="container">
|
|
<h2>Tracks</h2>
|
|
<div class="columns">\n`;
|
|
trackList.forEach(track => {
|
|
console.log("track", track);
|
|
var trackData = trackLineToObject(track.info.headers, track.info.l);
|
|
console.log("trackData", trackData);
|
|
html += `
|
|
<div class="column col-12">
|
|
<div class="card-header">
|
|
<div class="card-title h5">Track ${track.number}</div>
|
|
<div class="card-subtitle text-gray">${trackData.Time?trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }):"No track data"}</div>
|
|
</div>
|
|
${trackData.Latitude ? `
|
|
<div class="card-image">
|
|
<iframe
|
|
width="100%"
|
|
height="250"
|
|
frameborder="0" style="border:0"
|
|
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBxTcwrrVOh2piz7EmIs1Xn4FsRxJWeVH4&q=${trackData.Latitude},${trackData.Longitude}&zoom=10" allowfullscreen>
|
|
</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>
|
|
`;
|
|
});
|
|
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>
|
|
`;
|
|
}
|
|
html += `
|
|
</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>
|
|
</div>
|
|
</div>`;
|
|
domTracks.innerHTML = html;
|
|
document.getElementById("settings-allow-no-gps").addEventListener("change",event=>{
|
|
var allowNoGPS = event.target.checked;
|
|
localStorage.setItem("recorder-allow-no-gps", allowNoGPS);
|
|
});
|
|
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}`));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function onInit() {
|
|
getTrackList();
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|