2024-09-09 21:01:07 +00:00
|
|
|
{
|
|
|
|
const Layout = require("Layout");
|
2024-09-22 21:13:03 +00:00
|
|
|
const time_utils = require("time_utils");
|
2024-09-22 20:47:20 +00:00
|
|
|
const exs = require("exstats").getStats(
|
2024-09-22 21:13:03 +00:00
|
|
|
["dist", "pacec"],
|
2024-09-22 20:47:20 +00:00
|
|
|
{
|
|
|
|
notify: {
|
|
|
|
dist: {
|
|
|
|
increment: 1000,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
);
|
2024-10-10 19:30:44 +00:00
|
|
|
const S = require("Storage");
|
2024-09-09 21:01:07 +00:00
|
|
|
|
|
|
|
let drawTimeout: TimeoutId | undefined;
|
|
|
|
|
2024-10-10 17:02:33 +00:00
|
|
|
type Dist = number & { brand: 'dist' };
|
|
|
|
type Time = number & { brand: 'time' };
|
|
|
|
|
|
|
|
type Split = {
|
|
|
|
dist: Dist,
|
|
|
|
time: Time,
|
|
|
|
};
|
|
|
|
|
|
|
|
const splits: Split[] = []; // times
|
2024-09-09 21:01:07 +00:00
|
|
|
let splitOffset = 0, splitOffsetPx = 0;
|
|
|
|
|
|
|
|
const GPS_TIMEOUT_MS = 30000;
|
|
|
|
|
|
|
|
const layout = new Layout({
|
|
|
|
type: "v",
|
|
|
|
c: [
|
|
|
|
{
|
|
|
|
type: "txt",
|
|
|
|
font: "6x8:2",
|
|
|
|
label: "Pace",
|
|
|
|
id: "paceLabel",
|
|
|
|
pad: 4
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "txt",
|
|
|
|
font: "Vector:40",
|
|
|
|
label: "",
|
|
|
|
id: "pace",
|
|
|
|
halign: 0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "txt",
|
|
|
|
font: "6x8:2",
|
|
|
|
label: "Time",
|
|
|
|
id: "timeLabel",
|
|
|
|
pad: 4
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "txt",
|
|
|
|
font: "Vector:40",
|
|
|
|
label: "",
|
|
|
|
id: "time",
|
|
|
|
halign: 0
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}, {
|
|
|
|
lazy: true
|
|
|
|
});
|
|
|
|
|
|
|
|
const draw = () => {
|
2024-09-22 20:47:20 +00:00
|
|
|
if (!exs.state.active) {
|
2024-09-09 21:01:07 +00:00
|
|
|
// no draw-timeout here, only on user interaction
|
|
|
|
drawSplits();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (drawTimeout) clearTimeout(drawTimeout);
|
|
|
|
drawTimeout = setTimeout(draw, 1000);
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
let pace: string;
|
2024-09-22 20:47:20 +00:00
|
|
|
if ("time" in exs.state.thisGPS
|
|
|
|
&& now - (exs.state.thisGPS.time as unknown as number) < GPS_TIMEOUT_MS)
|
|
|
|
{
|
|
|
|
pace = exs.stats.pacec.getString()
|
2024-09-09 21:01:07 +00:00
|
|
|
}else{
|
|
|
|
pace = "No GPS";
|
|
|
|
}
|
|
|
|
|
2024-10-10 17:09:10 +00:00
|
|
|
layout["time"]!.label = formatDuration(exs.state.duration);
|
2024-09-09 21:01:07 +00:00
|
|
|
layout["pace"]!.label = pace;
|
|
|
|
layout.render();
|
|
|
|
};
|
|
|
|
|
2024-10-10 17:09:10 +00:00
|
|
|
const pad2 = (n: number) => `0${n}`.substr(-2);
|
|
|
|
|
|
|
|
const formatDuration = (ms: number) => {
|
|
|
|
const tm = time_utils.decodeTime(ms);
|
|
|
|
if(tm.h)
|
|
|
|
return `${tm.h}:${pad2(tm.m)}:${pad2(tm.s)}`;
|
|
|
|
return `${pad2(tm.m)}:${pad2(tm.s)}`;
|
|
|
|
};
|
|
|
|
|
2024-10-14 17:38:06 +00:00
|
|
|
// divide by actual distance, scale to milliseconds
|
|
|
|
const calculatePace = (split: Split) => formatDuration(split.time / split.dist * 1000);
|
|
|
|
|
2024-09-09 21:01:07 +00:00
|
|
|
const drawSplits = () => {
|
|
|
|
g.clearRect(Bangle.appRect);
|
|
|
|
|
|
|
|
const barSize = 20;
|
|
|
|
const barSpacing = 10;
|
|
|
|
const w = g.getWidth();
|
|
|
|
const h = g.getHeight();
|
|
|
|
|
2024-10-10 17:02:33 +00:00
|
|
|
const max = splits.reduce((a, s) => Math.max(a, s.time), 0);
|
2024-09-09 21:01:07 +00:00
|
|
|
|
|
|
|
g.setFont("6x8", 2).setFontAlign(-1, -1);
|
|
|
|
|
2024-11-11 12:05:56 +00:00
|
|
|
let y = Bangle.appRect.y + barSpacing / 2;
|
|
|
|
g
|
|
|
|
.setColor(g.theme.fg)
|
|
|
|
.drawString(formatDuration(exs.state.duration), 0, y);
|
|
|
|
|
2024-09-09 21:01:07 +00:00
|
|
|
let i = 0;
|
|
|
|
for(; ; i++) {
|
|
|
|
const split = splits[i + splitOffset];
|
|
|
|
if (split == null) break;
|
|
|
|
|
2024-11-11 12:05:56 +00:00
|
|
|
const y = Bangle.appRect.y + (i + 1) * (barSize + barSpacing) + barSpacing / 2;
|
2024-09-09 21:01:07 +00:00
|
|
|
if (y > h) break;
|
|
|
|
|
2024-10-10 17:02:33 +00:00
|
|
|
const size = w * split.time / max; // Scale bar height based on pace
|
2024-09-09 21:01:07 +00:00
|
|
|
g.setColor("#00f").fillRect(0, y, size, y + barSize);
|
|
|
|
|
2024-10-10 17:02:33 +00:00
|
|
|
const splitPace = calculatePace(split); // Pace per km
|
2024-11-11 12:05:56 +00:00
|
|
|
g.setColor(g.theme.fg)
|
2024-10-10 17:08:53 +00:00
|
|
|
drawSplit(i, y, splitPace);
|
2024-09-09 21:01:07 +00:00
|
|
|
}
|
|
|
|
|
2024-09-22 20:47:20 +00:00
|
|
|
const pace = exs.stats.pacec.getString();
|
|
|
|
|
2024-11-11 12:05:56 +00:00
|
|
|
y = Bangle.appRect.y + (i + 1) * (barSize + barSpacing) + barSpacing / 2;
|
2024-10-10 17:08:53 +00:00
|
|
|
drawSplit(i, y, pace);
|
|
|
|
};
|
|
|
|
|
2024-11-11 12:05:56 +00:00
|
|
|
const drawSplit = (i: number, y: number, pace: number | string) =>
|
2024-10-10 17:08:53 +00:00
|
|
|
g
|
|
|
|
.drawString(
|
|
|
|
`${i + 1 + splitOffset} ${typeof pace === "number" ? pace.toFixed(2) : pace}`,
|
|
|
|
0,
|
|
|
|
y
|
|
|
|
);
|
2024-09-09 21:01:07 +00:00
|
|
|
|
|
|
|
const pauseRun = () => {
|
2024-09-22 20:47:20 +00:00
|
|
|
exs.stop();
|
2024-09-09 21:01:07 +00:00
|
|
|
Bangle.setGPSPower(0, "pace")
|
|
|
|
draw();
|
|
|
|
};
|
|
|
|
|
|
|
|
const resumeRun = () => {
|
2024-09-22 21:13:36 +00:00
|
|
|
exs.resume();
|
2024-09-09 21:01:07 +00:00
|
|
|
Bangle.setGPSPower(1, "pace");
|
|
|
|
|
|
|
|
g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine
|
|
|
|
layout.forgetLazyState();
|
|
|
|
draw();
|
|
|
|
};
|
|
|
|
|
2024-09-22 20:47:20 +00:00
|
|
|
const onButton = () => {
|
|
|
|
if (exs.state.active)
|
|
|
|
pauseRun();
|
|
|
|
else
|
|
|
|
resumeRun();
|
2024-09-09 21:01:07 +00:00
|
|
|
};
|
|
|
|
|
2024-10-10 17:09:24 +00:00
|
|
|
exs.start(); // aka reset
|
|
|
|
|
2024-09-22 20:47:20 +00:00
|
|
|
exs.stats.dist.on("notify", (dist) => {
|
2024-10-10 19:31:17 +00:00
|
|
|
const prev = { time: 0, dist: 0 };
|
|
|
|
for(const s of splits){
|
|
|
|
prev.time += s.time;
|
|
|
|
prev.dist += s.dist;
|
|
|
|
}
|
|
|
|
|
2024-09-22 20:47:20 +00:00
|
|
|
const totalDist = dist.getValue();
|
2024-10-10 19:31:17 +00:00
|
|
|
let thisSplit = totalDist - prev.dist;
|
|
|
|
let thisTime = exs.state.duration - prev.time;
|
2024-09-22 20:47:20 +00:00
|
|
|
|
2024-10-10 17:02:33 +00:00
|
|
|
while(thisSplit > 1000) {
|
|
|
|
splits.push({ dist: thisSplit as Dist, time: thisTime as Time });
|
|
|
|
thisTime = 0; // if we've jumped more than 1k, credit the time to the first split
|
2024-09-22 20:47:20 +00:00
|
|
|
thisSplit -= 1000;
|
2024-09-09 21:01:07 +00:00
|
|
|
}
|
2024-10-10 17:02:59 +00:00
|
|
|
|
|
|
|
// subtract <how much we're over> off the next split notify
|
|
|
|
exs.state.notify.dist.next -= thisSplit;
|
2024-10-10 19:30:44 +00:00
|
|
|
|
|
|
|
S.writeJSON("pace.json", { splits });
|
2024-09-22 20:47:20 +00:00
|
|
|
});
|
2024-09-09 21:01:07 +00:00
|
|
|
|
|
|
|
Bangle.on('lock', locked => {
|
|
|
|
// treat an unlock (while running) as a pause
|
2024-09-22 20:47:20 +00:00
|
|
|
if(!locked && exs.state.active) onButton();
|
2024-09-09 21:01:07 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
setWatch(() => onButton(), BTN1, { repeat: true });
|
|
|
|
|
|
|
|
Bangle.on('drag', e => {
|
2024-09-22 20:47:20 +00:00
|
|
|
if (exs.state.active || e.b === 0) return;
|
2024-09-09 21:01:07 +00:00
|
|
|
|
|
|
|
splitOffsetPx -= e.dy;
|
|
|
|
if (splitOffsetPx > 20) {
|
|
|
|
if (splitOffset < splits.length-3) splitOffset++, Bangle.buzz(30);
|
|
|
|
splitOffsetPx = 0;
|
|
|
|
} else if (splitOffsetPx < -20) {
|
|
|
|
if (splitOffset > 0) splitOffset--, Bangle.buzz(30);
|
|
|
|
splitOffsetPx = 0;
|
|
|
|
}
|
|
|
|
draw();
|
|
|
|
});
|
|
|
|
|
2024-10-10 19:36:00 +00:00
|
|
|
Bangle.on('twist', () => {
|
|
|
|
Bangle.setBacklight(1);
|
|
|
|
});
|
|
|
|
|
2024-09-09 21:01:07 +00:00
|
|
|
Bangle.loadWidgets();
|
|
|
|
Bangle.drawWidgets();
|
|
|
|
|
|
|
|
g.clearRect(Bangle.appRect);
|
|
|
|
draw();
|
|
|
|
}
|