Fix back-gesture app exit, screen wake lock, multi-segment tracks
Navigation:
- _modalPopHandled flag prevents global popstate handler from firing
on the same event that modal popHandler already processed
- Fixes: back from fullscreen map/chart no longer exits the app
Screen wake lock:
- JS: navigator.wakeLock.request("screen") on recording screen
- Kotlin: FLAG_KEEP_SCREEN_ON on GPS start, clearFlags on stop
- Screen stays on while recording is visible in foreground
- Released when navigating away from recording screen
Multi-segment tracks:
- GPS gaps > gapThresholdSec (default 30, configurable) split track
into separate polyline segments on the map
- Each segment has own green start / red end markers
- Shows clearly where GPS signal was lost vs continuous tracking
Settings:
- "Макс. пауза GPS (сек)" — gap threshold for segment splitting
- Physics constants (aero drag, air density, pedal efficiency)
moved from hardcoded to GPS.config + settings UI
This commit is contained in:
parent
7cbfd98e27
commit
d7527bfd45
3 changed files with 57 additions and 16 deletions
|
|
@ -10,6 +10,7 @@ GPS.config = {
|
|||
minAccuracyM: 20, // skip fixes less accurate than this
|
||||
minDistanceM: 1, // skip points closer than this
|
||||
maxSpeedKmh: 60, // above this = GPS glitch, skip segment
|
||||
gapThresholdSec: 30, // gap longer than this = new track segment
|
||||
airDrag: 0.6, // аэродинамическое сопротивление (CdA, м2). Город=0.6, спорт=0.4
|
||||
airDensity: 1.225, // плотность воздуха (кг/м3). Уровень моря=1.225, горы=1.0
|
||||
pedalEfficiency: 25, // КПД педалирования (%). Человек ~25%
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ function persistNavStack() {
|
|||
try { localStorage.setItem("baldav_nav_stack", JSON.stringify(navStack)); } catch {}
|
||||
}
|
||||
|
||||
// Flag: modal popHandler already handled this popstate, global handler should skip
|
||||
let _modalPopHandled = false;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Boot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -103,6 +106,9 @@ async function init() {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderScreen(name, fromPopstate) {
|
||||
// Release screen wake lock when leaving recording screen
|
||||
if (window._screenLock) { try { window._screenLock.release(); } catch {} window._screenLock = null; }
|
||||
|
||||
const prevScreen = state.screen;
|
||||
state.screen = name;
|
||||
// Push to nav stack and browser history (unless triggered by popstate)
|
||||
|
|
@ -672,7 +678,8 @@ function showModal(html, onSubmit) {
|
|||
history.pushState({ modal: true }, "");
|
||||
return;
|
||||
}
|
||||
// Nothing fullscreen — close modal
|
||||
// Nothing fullscreen — close modal. Set flag so global handler skips this event.
|
||||
_modalPopHandled = true;
|
||||
window.removeEventListener("popstate", popHandler);
|
||||
overlay.remove();
|
||||
};
|
||||
|
|
@ -1731,6 +1738,13 @@ async function renderGpsScreen(container) {
|
|||
function renderRecordingInline(container) {
|
||||
if (!GPS.recording.active) { renderScreen("gps"); return; }
|
||||
|
||||
// Keep screen on while recording is visible
|
||||
if (navigator.wakeLock) {
|
||||
navigator.wakeLock.request("screen").then(lock => {
|
||||
window._screenLock = lock;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="rec-screen">
|
||||
<div id="rec-map" class="rec-map"></div>
|
||||
|
|
@ -2203,14 +2217,30 @@ async function showSessionDetail(sessionId) {
|
|||
const tN = new Date(points[points.length - 1].ts).getTime();
|
||||
const tRange = tN - t0 || 1;
|
||||
|
||||
const sortedCoords = [];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
sortedCoords.push([points[i].lat, points[i].lon]);
|
||||
// Split into segments by GPS gap threshold
|
||||
const gapSec = GPS.config.gapThresholdSec ?? 30;
|
||||
const segments = []; // array of arrays of [lat, lon]
|
||||
let curSeg = [[points[0].lat, points[0].lon]];
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const gap = (new Date(points[i].ts) - new Date(points[i-1].ts)) / 1000;
|
||||
if (gap > gapSec) {
|
||||
segments.push(curSeg);
|
||||
curSeg = [];
|
||||
}
|
||||
curSeg.push([points[i].lat, points[i].lon]);
|
||||
}
|
||||
segments.push(curSeg);
|
||||
|
||||
// Polyline first (underneath)
|
||||
L.polyline(sortedCoords, { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map);
|
||||
L.polyline(sortedCoords, { color: "#bb86fc", weight: 5 }).addTo(map);
|
||||
// Draw each segment as separate polyline with start/end markers
|
||||
const allCoords = [];
|
||||
segments.forEach(seg => {
|
||||
if (seg.length < 2) { allCoords.push(...seg); return; }
|
||||
L.polyline(seg, { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map);
|
||||
L.polyline(seg, { color: "#bb86fc", weight: 5 }).addTo(map);
|
||||
L.circleMarker(seg[0], { radius: 5, color: "#4caf50", fillColor: "#4caf50", fillOpacity: 1 }).addTo(map);
|
||||
L.circleMarker(seg[seg.length - 1], { radius: 5, color: "#f44336", fillColor: "#f44336", fillOpacity: 1 }).addTo(map);
|
||||
allCoords.push(...seg);
|
||||
});
|
||||
|
||||
// Invisible but tappable hit-targets on top of track
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
|
|
@ -2224,15 +2254,11 @@ async function showSessionDetail(sessionId) {
|
|||
}).bindPopup(info, { closeButton: false, className: "point-popup" }).addTo(map);
|
||||
}
|
||||
|
||||
// Start/end
|
||||
if (sortedCoords.length > 1) {
|
||||
L.circleMarker(sortedCoords[0], { radius: 6, color: "#4caf50", fillColor: "#4caf50", fillOpacity: 1 }).addTo(map);
|
||||
L.circleMarker(sortedCoords[sortedCoords.length - 1], { radius: 6, color: "#f44336", fillColor: "#f44336", fillOpacity: 1 }).addTo(map);
|
||||
map.fitBounds(L.latLngBounds(sortedCoords).pad(0.1));
|
||||
// Fit map to all points
|
||||
if (allCoords.length > 1) {
|
||||
map.fitBounds(L.latLngBounds(allCoords).pad(0.1));
|
||||
}
|
||||
|
||||
const allCoords = sortedCoords;
|
||||
mapEl._bounds = L.latLngBounds(sortedCoords).pad(0.1); // save for collapse fitBounds
|
||||
mapEl._bounds = allCoords.length > 1 ? L.latLngBounds(allCoords).pad(0.1) : null;
|
||||
|
||||
// Fullscreen toggle button on map
|
||||
// Fullscreen map toggle
|
||||
|
|
@ -2752,6 +2778,13 @@ async function renderSettings(container) {
|
|||
</div>
|
||||
<input id="s-max-speed" type="number" step="1" value="${GPS.config.maxSpeedKmh}" style="width:60px;text-align:right;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg);padding:4px 6px;">
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-title">Макс. пауза GPS (сек)</div>
|
||||
<div class="settings-item-desc">Пробел длиннее = новый сегмент</div>
|
||||
</div>
|
||||
<input id="s-gap-threshold" type="number" step="5" value="${GPS.config.gapThresholdSec}" style="width:60px;text-align:right;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg);padding:4px 6px;">
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-title">Аэросопротивление</div>
|
||||
|
|
@ -2819,7 +2852,7 @@ async function renderSettings(container) {
|
|||
};
|
||||
|
||||
// GPS config inputs
|
||||
const gpsInputs = ["s-min-speed", "s-min-acc", "s-min-dist", "s-max-speed", "s-air-drag", "s-air-density", "s-pedal-eff"];
|
||||
const gpsInputs = ["s-min-speed", "s-min-acc", "s-min-dist", "s-max-speed", "s-gap-threshold", "s-air-drag", "s-air-density", "s-pedal-eff"];
|
||||
for (const id of gpsInputs) {
|
||||
const el = container.querySelector("#" + id);
|
||||
if (el) el.onchange = () => {
|
||||
|
|
@ -2827,6 +2860,7 @@ async function renderSettings(container) {
|
|||
GPS.config.minAccuracyM = parseFloat(container.querySelector("#s-min-acc").value);
|
||||
GPS.config.minDistanceM = parseFloat(container.querySelector("#s-min-dist").value);
|
||||
GPS.config.maxSpeedKmh = parseFloat(container.querySelector("#s-max-speed").value) || 60;
|
||||
GPS.config.gapThresholdSec = parseFloat(container.querySelector("#s-gap-threshold").value) || 30;
|
||||
GPS.config.airDrag = parseFloat(container.querySelector("#s-air-drag").value) || 0.6;
|
||||
GPS.config.airDensity = parseFloat(container.querySelector("#s-air-density").value) || 1.225;
|
||||
GPS.config.pedalEfficiency = parseFloat(container.querySelector("#s-pedal-eff").value) || 25;
|
||||
|
|
@ -3253,6 +3287,8 @@ function esc(str) {
|
|||
|
||||
// Global back navigation between screens
|
||||
window.addEventListener("popstate", (e) => {
|
||||
// Modal popHandler already handled this event — skip
|
||||
if (_modalPopHandled) { _modalPopHandled = false; return; }
|
||||
// If a modal is open, don't handle here (modal has its own handler)
|
||||
if (document.querySelector(".modal-overlay")) return;
|
||||
// Chart fullscreen on screen (not in modal) — collapse it
|
||||
|
|
|
|||
|
|
@ -125,12 +125,16 @@ class MainActivity : TauriActivity() {
|
|||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
// Keep screen on while recording
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
cmd == "stop" -> {
|
||||
Log.d("BalDav", "Stopping GPS service")
|
||||
val intent = Intent(this, GpsRecorderService::class.java)
|
||||
intent.action = GpsRecorderService.ACTION_STOP
|
||||
startService(intent)
|
||||
// Allow screen to turn off
|
||||
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
cmd.startsWith("save:") -> {
|
||||
// save:filename.gpx — read content from save_content.txt, show SAF picker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue