diff --git a/frontend/js/gps.js b/frontend/js/gps.js index 87dca37..8b657b4 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -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% diff --git a/frontend/js/main.js b/frontend/js/main.js index 8068bb4..1a0e176 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -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 = `
@@ -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) {
+
+
+
Макс. пауза GPS (сек)
+
Пробел длиннее = новый сегмент
+
+ +
Аэросопротивление
@@ -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 diff --git a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt index ea05df4..ab08025 100644 --- a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt @@ -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