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:
4aiman 2026-05-27 15:29:22 +03:00
commit d7527bfd45
3 changed files with 57 additions and 16 deletions

View file

@ -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%

View file

@ -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

View file

@ -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