feat: max-speed marker scrubs timeline + timeline tick (FEAT-16)
- Clicking the amber max-speed map marker now jumps the replay to that point - Add an amber tick on the scrubber at the max-speed position (clickable too) - Both on Pi and server portal
This commit is contained in:
@@ -71,8 +71,11 @@
|
|||||||
<button id="trip-play" class="btn btn-primary btn-sm" type="button" aria-label="Play">
|
<button id="trip-play" class="btn btn-primary btn-sm" type="button" aria-label="Play">
|
||||||
<i class="fas fa-play" aria-hidden="true"></i>
|
<i class="fas fa-play" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<input id="trip-scrubber" class="form-range flex-grow-1" type="range"
|
<div class="position-relative flex-grow-1">
|
||||||
min="0" max="0" value="0" aria-label="Trip replay timeline">
|
<input id="trip-scrubber" class="form-range w-100" type="range"
|
||||||
|
min="0" max="0" value="0" aria-label="Trip replay timeline">
|
||||||
|
<div id="trip-max-tick" class="trip-max-tick" title="Max speed" hidden></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap justify-content-between gap-2 small mt-2">
|
<div class="d-flex flex-wrap justify-content-between gap-2 small mt-2">
|
||||||
<strong id="trip-replay-time"></strong>
|
<strong id="trip-replay-time"></strong>
|
||||||
|
|||||||
+38
-5
@@ -35,7 +35,8 @@
|
|||||||
|
|
||||||
// ── Core state ──────────────────────────────────────────────────────
|
// ── Core state ──────────────────────────────────────────────────────
|
||||||
let st = {
|
let st = {
|
||||||
route: null, marker: null, points: [], timer: null, ctrl: null, activeRow: null
|
route: null, marker: null, points: [], timer: null, ctrl: null, activeRow: null,
|
||||||
|
maxIndex: null
|
||||||
};
|
};
|
||||||
let _cache = {};
|
let _cache = {};
|
||||||
let _geoTimer = null, _geoCtrl = null, _lastKey = null;
|
let _geoTimer = null, _geoCtrl = null, _lastKey = null;
|
||||||
@@ -59,7 +60,9 @@
|
|||||||
stopTm();
|
stopTm();
|
||||||
if (st.route) { map.removeLayer(st.route); st.route = null; }
|
if (st.route) { map.removeLayer(st.route); st.route = null; }
|
||||||
if (st.marker) { map.removeLayer(st.marker); st.marker = null; }
|
if (st.marker) { map.removeLayer(st.marker); st.marker = null; }
|
||||||
st.points = []; _cache = {}; _lastKey = null;
|
st.points = []; _cache = {}; _lastKey = null; st.maxIndex = null;
|
||||||
|
var maxTick = document.getElementById("trip-max-tick");
|
||||||
|
if (maxTick) maxTick.hidden = true;
|
||||||
if (_geoCtrl) { _geoCtrl.abort(); _geoCtrl = null; }
|
if (_geoCtrl) { _geoCtrl.abort(); _geoCtrl = null; }
|
||||||
clearTimeout(_geoTimer);
|
clearTimeout(_geoTimer);
|
||||||
timeline.hidden = true; summary.textContent = "Select a trip";
|
timeline.hidden = true; summary.textContent = "Select a trip";
|
||||||
@@ -149,6 +152,27 @@
|
|||||||
showPt(0);
|
showPt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Max-speed marker helpers ────────────────────────────────────────
|
||||||
|
function jumpToMax() {
|
||||||
|
if (st.maxIndex == null) return;
|
||||||
|
stopTm();
|
||||||
|
showPt(st.maxIndex);
|
||||||
|
}
|
||||||
|
function placeMaxTick() {
|
||||||
|
var tick = document.getElementById("trip-max-tick");
|
||||||
|
var scrubber = document.getElementById("trip-scrubber");
|
||||||
|
if (!tick || !scrubber) return;
|
||||||
|
if (st.maxIndex == null || !st.points || st.points.length < 2) {
|
||||||
|
tick.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ratio = st.maxIndex / (st.points.length - 1);
|
||||||
|
var thumb = 16; // approximate range thumb width
|
||||||
|
var usable = Math.max(1, scrubber.clientWidth - thumb);
|
||||||
|
tick.style.left = (thumb / 2 + ratio * usable) + "px";
|
||||||
|
tick.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Trip loader ─────────────────────────────────────────────────────
|
// ── Trip loader ─────────────────────────────────────────────────────
|
||||||
var _deb;
|
var _deb;
|
||||||
function loadTrip(id) {
|
function loadTrip(id) {
|
||||||
@@ -172,10 +196,14 @@
|
|||||||
L.marker(coords[0]).bindPopup("Start").addTo(st.route);
|
L.marker(coords[0]).bindPopup("Start").addTo(st.route);
|
||||||
L.marker(coords[coords.length - 1]).bindPopup("End").addTo(st.route);
|
L.marker(coords[coords.length - 1]).bindPopup("End").addTo(st.route);
|
||||||
}
|
}
|
||||||
// Pin the fastest point on the route.
|
// Pin the fastest point on the route, remembering its index so
|
||||||
var maxPt = null;
|
// clicking the marker scrubs the timeline to that point.
|
||||||
|
var maxPt = null; st.maxIndex = null;
|
||||||
for (var i = 0; i < d.points.length; i++) {
|
for (var i = 0; i < d.points.length; i++) {
|
||||||
if (!maxPt || d.points[i].speed > maxPt.speed) maxPt = d.points[i];
|
if (!maxPt || d.points[i].speed > maxPt.speed) {
|
||||||
|
maxPt = d.points[i];
|
||||||
|
st.maxIndex = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (maxPt && maxPt.speed > 0) {
|
if (maxPt && maxPt.speed > 0) {
|
||||||
L.circleMarker([maxPt.latitude, maxPt.longitude], {
|
L.circleMarker([maxPt.latitude, maxPt.longitude], {
|
||||||
@@ -188,6 +216,7 @@
|
|||||||
.bindTooltip("Max speed: " + fS(maxPt.speed), {
|
.bindTooltip("Max speed: " + fS(maxPt.speed), {
|
||||||
permanent: true, direction: "top", offset: [0, -8],
|
permanent: true, direction: "top", offset: [0, -8],
|
||||||
})
|
})
|
||||||
|
.on("click", jumpToMax)
|
||||||
.addTo(st.route);
|
.addTo(st.route);
|
||||||
}
|
}
|
||||||
st.route.addTo(map);
|
st.route.addTo(map);
|
||||||
@@ -202,12 +231,16 @@
|
|||||||
};
|
};
|
||||||
summary.textContent = fK(d.trip.distance_km) + " \u00b7 " + fD(d.trip.duration_seconds) + " \u00b7 avg " + fS(d.trip.average_speed_kmh) + " \u00b7 max " + fS(d.trip.max_speed_kmh) + " \u00b7 " + d.points.length + " pts";
|
summary.textContent = fK(d.trip.distance_km) + " \u00b7 " + fD(d.trip.duration_seconds) + " \u00b7 avg " + fS(d.trip.average_speed_kmh) + " \u00b7 max " + fS(d.trip.max_speed_kmh) + " \u00b7 " + d.points.length + " pts";
|
||||||
initTL(d.points);
|
initTL(d.points);
|
||||||
|
placeMaxTick();
|
||||||
} catch (e) { if (e.name !== "AbortError") showErr(e.message); }
|
} catch (e) { if (e.name !== "AbortError") showErr(e.message); }
|
||||||
finally { if (!sig.aborted) setLoading(false); }
|
finally { if (!sig.aborted) setLoading(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scrubber & play ─────────────────────────────────────────────────
|
// ── Scrubber & play ─────────────────────────────────────────────────
|
||||||
scrubber.addEventListener("input", function () { stopTm(); showPt(Number(scrubber.value)); });
|
scrubber.addEventListener("input", function () { stopTm(); showPt(Number(scrubber.value)); });
|
||||||
|
var maxTick = document.getElementById("trip-max-tick");
|
||||||
|
if (maxTick) maxTick.addEventListener("click", jumpToMax);
|
||||||
|
window.addEventListener("resize", placeMaxTick);
|
||||||
playButton.addEventListener("click", function () {
|
playButton.addEventListener("click", function () {
|
||||||
if (st.timer) { stopTm(); return; }
|
if (st.timer) { stopTm(); return; }
|
||||||
if (Number(scrubber.value) >= st.points.length - 1) showPt(0);
|
if (Number(scrubber.value) >= st.points.length - 1) showPt(0);
|
||||||
|
|||||||
@@ -334,6 +334,21 @@ body {
|
|||||||
padding-bottom: .375rem;
|
padding-bottom: .375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Max-speed tick overlaid on the trip replay scrubber. */
|
||||||
|
.trip-max-tick {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f6c23e;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Dashcam / video ─────────────────────────────────────────────────────── */
|
/* ── Dashcam / video ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
/* Desktop: truncate long filenames */
|
/* Desktop: truncate long filenames */
|
||||||
|
|||||||
@@ -68,8 +68,11 @@
|
|||||||
<button id="trip-play" class="btn btn-primary btn-sm" type="button" aria-label="Play">
|
<button id="trip-play" class="btn btn-primary btn-sm" type="button" aria-label="Play">
|
||||||
<i class="fas fa-play" aria-hidden="true"></i>
|
<i class="fas fa-play" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<input id="trip-scrubber" class="form-range flex-grow-1" type="range"
|
<div class="position-relative flex-grow-1">
|
||||||
min="0" max="0" value="0" aria-label="Trip replay timeline">
|
<input id="trip-scrubber" class="form-range w-100" type="range"
|
||||||
|
min="0" max="0" value="0" aria-label="Trip replay timeline">
|
||||||
|
<div id="trip-max-tick" class="trip-max-tick" title="Max speed" hidden></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap justify-content-between gap-2 small mt-2">
|
<div class="d-flex flex-wrap justify-content-between gap-2 small mt-2">
|
||||||
<strong id="trip-replay-time"></strong>
|
<strong id="trip-replay-time"></strong>
|
||||||
|
|||||||
Reference in New Issue
Block a user