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