BangleApps/apps/recorder/interface.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>