`, async overlay => {
- const selected = overlay.querySelector(".recipe-pick-item.selected");
+ let selected = overlay.querySelector(".recipe-pick-item.selected");
if (!selected) return false;
- const rid = parseInt(selected.dataset.rid);
- const time = overlay.querySelector("#rfr-time").value;
- const amountG = parseAmount(overlay.querySelector("#rfr-amount").value);
+ let rid = parseInt(selected.dataset.rid);
+ let time = overlay.querySelector("#rfr-time").value;
+ let amountG = parseAmount(overlay.querySelector("#rfr-amount").value);
if (isNaN(amountG) || amountG <= 0) return false;
await DB.addFoodFromRecipe(state.date, time, rid, amountG);
});
// Load recipes into the list
- const overlay = [...document.querySelectorAll(".modal-overlay")].pop();
- const listEl = overlay.querySelector("#rfr-list");
+ let overlay = [...document.querySelectorAll(".modal-overlay")].pop();
+ let listEl = overlay.querySelector("#rfr-list");
(async () => {
- const recipes = await DB.getRecipes();
+ let recipes = await DB.getRecipes();
if (!recipes.length) {
listEl.innerHTML = `
Нет рецептов. Создайте в Продукты → Рецепты.
`;
return;
}
// Calculate nutrition for display
- const items = [];
- for (const r of recipes) {
- const n = await DB.calculateRecipeNutrition(r.id);
+ let items = [];
+ for (let r of recipes) {
+ let n = await DB.calculateRecipeNutrition(r.id);
items.push({ ...r, nutr: n });
}
@@ -2299,15 +2305,15 @@ function showFoodFromRecipeModal() {
`
).join("");
- const amountInput = overlay.querySelector("#rfr-amount");
- const hintEl = overlay.querySelector("#rfr-hint");
+ let amountInput = overlay.querySelector("#rfr-amount");
+ let hintEl = overlay.querySelector("#rfr-hint");
listEl.querySelectorAll(".recipe-pick-item").forEach(item => {
item.onclick = () => {
listEl.querySelectorAll(".recipe-pick-item").forEach(i => i.classList.remove("selected"));
item.classList.add("selected");
- const r = items.find(x => x.id === parseInt(item.dataset.rid));
+ let r = items.find(x => x.id === parseInt(item.dataset.rid));
if (r && r.nutr) {
- const servingG = Math.round(r.nutr.totalWeight / (r.servings || 1));
+ let servingG = Math.round(r.nutr.totalWeight / (r.servings || 1));
amountInput.value = servingG;
hintEl.textContent = `1 порция = ${servingG} г, ${r.nutr.kcal} кк`;
}
@@ -2327,10 +2333,10 @@ let gpsMarker = null;
async function renderGpsScreen(container) {
// Check Bluetooth state if BLE device is paired
- const bleAddr = await DB.getProfileValue("ble_device_addr");
+ let bleAddr = await DB.getProfileValue("ble_device_addr");
if (bleAddr) {
try {
- const btState = await window.__TAURI__.core.invoke("ble_check_bluetooth");
+ let btState = await window.__TAURI__.core.invoke("ble_check_bluetooth");
if (btState === "off") {
showToast("Bluetooth выключен");
} else if (btState === "no_adapter") {
@@ -2339,8 +2345,8 @@ async function renderGpsScreen(container) {
} catch {}
}
- const sessions = await DB.getGpsSessions(20);
- const isRec = GPS.recording.active;
+ let sessions = await DB.getGpsSessions(20);
+ let isRec = GPS.recording.active;
container.innerHTML = `
@@ -2396,12 +2402,12 @@ async function renderGpsScreen(container) {
if (GPS.recording.active) {
renderScreen("recording");
} else {
- const type = container.querySelector("#gps-type").value;
+ let type = container.querySelector("#gps-type").value;
localStorage.setItem("baldav_gps_type", type);
- const wkg = state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80;
+ let wkg = state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80;
// Get active bike for bike sessions — store bike data in recording state
if (type === "bike") {
- const bike = await DB.getActiveBike();
+ let bike = await DB.getActiveBike();
if (bike) {
GPS.recording.bikeId = bike.id;
GPS.recording.bikeWeightKg = bike.weight_kg ?? 0;
@@ -2420,14 +2426,14 @@ async function renderGpsScreen(container) {
// Session click → detail view
container.querySelectorAll(".gps-session-item").forEach(item => {
item.onclick = () => {
- const sid = parseInt(item.dataset.sid);
+ let sid = parseInt(item.dataset.sid);
showSessionDetail(sid);
};
});
// Three-dot menu
- const menuBtn = container.querySelector("#gps-menu-btn");
- const menu = container.querySelector("#gps-menu");
+ let menuBtn = container.querySelector("#gps-menu-btn");
+ let menu = container.querySelector("#gps-menu");
menuBtn.onclick = () => menu.classList.toggle("hidden");
document.addEventListener("click", e => {
if (!e.target.closest(".gps-menu-wrap")) menu.classList.add("hidden");
@@ -2444,7 +2450,7 @@ async function renderGpsScreen(container) {
// Clear tile cache
container.querySelector("#gps-clear-cache").onclick = async () => {
menu.classList.add("hidden");
- const count = await TileCache.count();
+ let count = await TileCache.count();
await TileCache.clear();
console.log("Cache cleared");
};
@@ -2454,17 +2460,17 @@ async function renderGpsScreen(container) {
menu.classList.add("hidden");
pickFile("", async (data, file) => {
console.log("[IMPORT] file:", file.name, "size:", data.byteLength || data.length, "type:", typeof data);
- const ext = file.name.split(".").pop().toLowerCase();
+ let ext = file.name.split(".").pop().toLowerCase();
console.log("[IMPORT] ext:", ext);
- const wkg = state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80;
+ let wkg = state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80;
try {
if (ext === "gpx") {
- const text = typeof data === "string" ? data : new TextDecoder().decode(data);
+ let text = typeof data === "string" ? data : new TextDecoder().decode(data);
await GPS.importGPX(text, wkg);
} else if (ext === "ftb" || ext === "zip") {
- const buf = data instanceof ArrayBuffer ? data : (typeof data === "string" ? new TextEncoder().encode(data).buffer : data);
+ let buf = data instanceof ArrayBuffer ? data : (typeof data === "string" ? new TextEncoder().encode(data).buffer : data);
console.log("[IMPORT] FTB buf size:", buf.byteLength);
- const count = await GPS.importFTB(buf, wkg);
+ let count = await GPS.importFTB(buf, wkg);
console.log("[IMPORT] done, count:", count);
} else {
console.error("[IMPORT] Unsupported file type: " + ext);
@@ -2485,7 +2491,7 @@ async function renderGpsScreen(container) {
// ---------------------------------------------------------------------------
async function showGoalMarkersModal() {
- const markers = await DB.getGoalMarkers();
+ let markers = await DB.getGoalMarkers();
let html = `
Цели (маркеры)
@@ -2507,9 +2513,9 @@ async function showGoalMarkersModal() {
// Hide save/cancel since this is a management modal
setTimeout(() => {
- const overlay = [...document.querySelectorAll(".modal-overlay")].pop();
+ let overlay = [...document.querySelectorAll(".modal-overlay")].pop();
if (!overlay) return;
- const btns = overlay.querySelector(".modal-btns");
+ let btns = overlay.querySelector(".modal-btns");
if (btns) btns.style.display = "none";
// Delete buttons
@@ -2553,10 +2559,10 @@ async function showAddGoalMarkerModal() {
`, async overlay => {
- const name = overlay.querySelector("#gm-name").value.trim();
- const lat = parseFloat(overlay.querySelector("#gm-lat").value);
- const lon = parseFloat(overlay.querySelector("#gm-lon").value);
- const notes = overlay.querySelector("#gm-notes").value.trim() || null;
+ let name = overlay.querySelector("#gm-name").value.trim();
+ let lat = parseFloat(overlay.querySelector("#gm-lat").value);
+ let lon = parseFloat(overlay.querySelector("#gm-lon").value);
+ let notes = overlay.querySelector("#gm-notes").value.trim() || null;
if (!name) return false;
if (isNaN(lat) || isNaN(lon)) return false;
await DB.addGoalMarker(name, lat, lon, notes);
@@ -2566,10 +2572,10 @@ async function showAddGoalMarkerModal() {
// Initialize map after DOM is ready
setTimeout(() => {
- const mapEl = document.querySelector("#gm-map");
- const coordsEl = document.querySelector("#gm-coords");
- const latEl = document.querySelector("#gm-lat");
- const lonEl = document.querySelector("#gm-lon");
+ let mapEl = document.querySelector("#gm-map");
+ let coordsEl = document.querySelector("#gm-coords");
+ let latEl = document.querySelector("#gm-lat");
+ let lonEl = document.querySelector("#gm-lon");
if (!mapEl) return;
// Center on user's last known position or default
@@ -2577,9 +2583,9 @@ async function showAddGoalMarkerModal() {
// Try to get last GPS point from recent sessions
DB.getGpsSessions(1).then(async sessions => {
if (sessions.length > 0) {
- const pts = await DB.getGpsPoints(sessions[0].id);
+ let pts = await DB.getGpsPoints(sessions[0].id);
if (pts.length > 0) {
- const last = pts[pts.length - 1];
+ let last = pts[pts.length - 1];
defaultLat = last.lat;
defaultLon = last.lon;
map.setView([defaultLat, defaultLon], 14);
@@ -2588,15 +2594,15 @@ async function showAddGoalMarkerModal() {
}).catch(() => {});
L.Icon.Default.imagePath = "lib/";
- const map = L.map(mapEl, { attributionControl: false }).setView([defaultLat, defaultLon], 14);
+ let map = L.map(mapEl, { attributionControl: false }).setView([defaultLat, defaultLon], 14);
addZoomDisplay(map);
- const CachedLayer = TileCache.createLayer();
+ let CachedLayer = TileCache.createLayer();
new CachedLayer({ maxZoom: 19 }).addTo(map);
let marker = null;
map.on("click", (e) => {
- const lat = e.latlng.lat;
- const lon = e.latlng.lng;
+ let lat = e.latlng.lat;
+ let lon = e.latlng.lng;
latEl.value = lat;
lonEl.value = lon;
coordsEl.textContent = `${lat.toFixed(5)}, ${lon.toFixed(5)}`;
@@ -2624,14 +2630,14 @@ async function showAddGoalMarkerModal() {
async function fetchElevations(points) {
try {
// Batch up to 100 points per request
- const elevations = new Array(points.length).fill(null);
+ let elevations = new Array(points.length).fill(null);
for (let i = 0; i < points.length; i += 100) {
- const batch = points.slice(i, i + 100);
- const lats = batch.map(p => p.lat.toFixed(5)).join(",");
- const lons = batch.map(p => p.lon.toFixed(5)).join(",");
- const resp = await fetch(`https://api.open-meteo.com/v1/elevation?latitude=${lats}&longitude=${lons}`);
+ let batch = points.slice(i, i + 100);
+ let lats = batch.map(p => p.lat.toFixed(5)).join(",");
+ let lons = batch.map(p => p.lon.toFixed(5)).join(",");
+ let resp = await fetch(`https://api.open-meteo.com/v1/elevation?latitude=${lats}&longitude=${lons}`);
if (!resp.ok) continue;
- const data = await resp.json();
+ let data = await resp.json();
if (data.elevation) {
for (let j = 0; j < data.elevation.length; j++) elevations[i + j] = data.elevation[j];
}
@@ -2644,7 +2650,7 @@ async function fetchElevations(points) {
}
function showDrawRouteScreen(container) {
- const points = []; // {lat, lon}
+ let points = []; // {lat, lon}
let markers = [];
let polyline = null;
@@ -2668,23 +2674,23 @@ function showDrawRouteScreen(container) {
`;
setTimeout(() => {
- const mapEl = document.querySelector("#draw-map");
+ let mapEl = document.querySelector("#draw-map");
if (!mapEl) return;
L.Icon.Default.imagePath = "lib/";
- const map = L.map(mapEl, { attributionControl: false }).setView([55.75, 37.62], 15);
- const CachedLayer = TileCache.createLayer();
+ let map = L.map(mapEl, { attributionControl: false }).setView([55.75, 37.62], 15);
+ let CachedLayer = TileCache.createLayer();
new CachedLayer({ maxZoom: 19 }).addTo(map);
// Try to center on last known position
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
map.setView([pos.coords.latitude, pos.coords.longitude], 15);
- }, () => {}, { enableHighAccuracy: false, timeout: 3000, maximumAge: 60000 });
+ }, () => {}, { enableHighAccuracy: false, timeout: 1000, maximumAge: 5000 });
}
- const distEl = document.querySelector("#draw-dist");
- const ptsEl = document.querySelector("#draw-pts");
+ let distEl = document.querySelector("#draw-dist");
+ let ptsEl = document.querySelector("#draw-pts");
function updateStats() {
let dist = 0;
@@ -2696,7 +2702,7 @@ function showDrawRouteScreen(container) {
}
function updatePolyline() {
- const latlngs = points.map(p => [p.lat, p.lon]);
+ let latlngs = points.map(p => [p.lat, p.lon]);
if (polyline) map.removeLayer(polyline);
if (latlngs.length >= 2) {
polyline = L.polyline(latlngs, { color: "#bb86fc", weight: 4 }).addTo(map);
@@ -2705,9 +2711,9 @@ function showDrawRouteScreen(container) {
// Map click → add point
map.on("click", e => {
- const { lat, lng } = e.latlng;
+ let { lat, lng } = e.latlng;
points.push({ lat, lon: lng });
- const marker = L.circleMarker([lat, lng], {
+ let marker = L.circleMarker([lat, lng], {
radius: 6, color: "#bb86fc", fillColor: "#bb86fc", fillOpacity: 1, weight: 2
}).addTo(map);
markers.push(marker);
@@ -2719,7 +2725,7 @@ function showDrawRouteScreen(container) {
document.querySelector("#draw-undo").onclick = () => {
if (points.length === 0) return;
points.pop();
- const m = markers.pop();
+ let m = markers.pop();
if (m) map.removeLayer(m);
updatePolyline();
updateStats();
@@ -2732,34 +2738,34 @@ function showDrawRouteScreen(container) {
document.querySelector("#draw-save").onclick = async () => {
if (points.length < 2) { showToast("Нужно минимум 2 точки"); return; }
- const saveBtn = document.querySelector("#draw-save");
+ let saveBtn = document.querySelector("#draw-save");
saveBtn.textContent = "Сохранение...";
saveBtn.disabled = true;
- const activityType = document.querySelector("#draw-type").value;
+ let activityType = document.querySelector("#draw-type").value;
// Get elevation for each point
- const needApi = [];
- for (const p of points) {
- const alt = await DB.findNearestAltitude(p.lat, p.lon, 50);
+ let needApi = [];
+ for (let p of points) {
+ let alt = await DB.findNearestAltitude(p.lat, p.lon, 50);
if (alt != null) { p.altitude = alt; }
else { needApi.push(p); }
}
if (needApi.length > 0) {
- const elevs = await fetchElevations(needApi);
+ let elevs = await fetchElevations(needApi);
if (elevs) needApi.forEach((p, i) => { if (elevs[i] != null) p.altitude = elevs[i]; });
}
- for (const p of points) { if (p.altitude == null) p.altitude = 0; }
+ for (let p of points) { if (p.altitude == null) p.altitude = 0; }
// Build points for DB with realistic timestamps based on activity speed
- const avgSpeedMs = (activityType === "bike" ? GPS.config.bikeAvgSpeedKmh
+ let avgSpeedMs = (activityType === "bike" ? GPS.config.bikeAvgSpeedKmh
: activityType === "run" ? GPS.config.runAvgSpeedKmh
: GPS.config.walkAvgSpeedKmh) / 3.6;
- const now = Date.now();
+ let now = Date.now();
let cumTime = 0;
- const dbPoints = points.map((p, i) => {
+ let dbPoints = points.map((p, i) => {
if (i > 0) {
- const d = GPS.distance(points[i-1].lat, points[i-1].lon, p.lat, p.lon);
+ let d = GPS.distance(points[i-1].lat, points[i-1].lon, p.lat, p.lon);
cumTime += d / avgSpeedMs; // seconds to cover this segment
}
return {
@@ -2772,17 +2778,17 @@ function showDrawRouteScreen(container) {
});
// Compute stats
- const latestW = await DB.getLatestWeight();
- const profile = await DB.getFullProfile();
- const weightKg = parseFloat(latestW?.weight_kg) || parseFloat(profile?.weight_kg) || 80;
- const bike = await DB.getActiveBike();
- const opts = { activityType, bikeWeightKg: bike?.weight_kg ?? 0, surfaceCrr: bike?.surface_type };
- const stats = GPS.computeStats(dbPoints, weightKg, opts);
+ let latestW = await DB.getLatestWeight();
+ let profile = await DB.getFullProfile();
+ let weightKg = parseFloat(latestW?.weight_kg) || parseFloat(profile?.weight_kg) || 80;
+ let bike = await DB.getActiveBike();
+ let opts = { activityType, bikeWeightKg: bike?.weight_kg ?? 0, surfaceCrr: bike?.surface_type };
+ let stats = GPS.computeStats(dbPoints, weightKg, opts);
// Create session
- const date = new Date().toISOString().slice(0, 10);
- const time = new Date().toTimeString().slice(0, 5);
- const session = {
+ let date = new Date().toISOString().slice(0, 10);
+ let time = new Date().toTimeString().slice(0, 5);
+ let session = {
id: now, date, start_time: time, end_time: time,
activity_type: activityType, source: "manual",
total_distance_m: Math.round(stats.distance),
@@ -2840,36 +2846,39 @@ function renderRecordingInline(container) {
`;
- setTimeout(() => {
- const mapEl = document.querySelector("#rec-map");
+
+ let mapEl = container.querySelector("#rec-map");
if (!mapEl) return;
L.Icon.Default.imagePath = "lib/";
// Use last known position from profile (reliable across sessions)
- const lastPt = GPS.recording.lastMovingPoint;
- const initLat = lastPt?.lat || parseFloat(state.profile.last_lat) || 47.47;
- const initLon = lastPt?.lon || parseFloat(state.profile.last_lon) || 40.07;
- const map = L.map(mapEl, { attributionControl: false, scrollWheelZoom: false, rotate: true, bearing: 0, compassBearing: false }).setView([initLat, initLon], 15);
+ let lastPt = GPS.recording.lastMovingPoint;
+ let initLat = lastPt?.lat || parseFloat(state.profile.last_lat) || 47.47;
+ let initLon = lastPt?.lon || parseFloat(state.profile.last_lon) || 40.07;
+ let map = L.map(mapEl, { attributionControl: false, scrollWheelZoom: false, rotate: true, bearing: 0, compassBearing: false }).setView([initLat, initLon], 15);
mapEl.addEventListener("wheel", (e) => {
if (e.ctrlKey) { e.preventDefault(); map.setZoom(map.getZoom() + (e.deltaY < 0 ? 1 : -1)); }
}, { passive: false });
addZoomDisplay(map);
- const CachedLayer = TileCache.createLayer();
+ let CachedLayer = TileCache.createLayer();
new CachedLayer({ maxZoom: 19 }).addTo(map);
// Force recalc after flex layout settles — fixes black screen on init
setTimeout(() => map.invalidateSize(), 200);
- const borderLine = L.polyline([], { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map);
- const trailLine = L.polyline([], { color: "#bb86fc", weight: 5 }).addTo(map);
+ let borderLine = L.polyline([], { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map);
+ let trailLine = L.polyline([], { color: "#bb86fc", weight: 5 }).addTo(map);
// Compass heading + landscape detection from device orientation
let currentLandscape = null; // null | 'right' | 'left'
let orientMode = "off"; // "off" | "auto" | "lock"
- const recScreen = document.querySelector('.rec-screen');
- const btnWrap = mapEl.parentElement;
+ let lastBeta = 0;
+ let lastGamma = 0;
+ let smoothedHeading = null;
+ let recScreen = document.querySelector('.rec-screen');
+ let btnWrap = mapEl.parentElement;
// Orient button — 3 states: X (off) → A (auto) → L (lock)
- const lockBtn = document.createElement("button");
+ let lockBtn = document.createElement("button");
lockBtn.className = "rec-map-btn rec-orient-lock";
lockBtn.textContent = "X";
lockBtn.onclick = () => {
@@ -2879,7 +2888,7 @@ function renderRecordingInline(container) {
// Reset landscape and bearing
currentLandscape = null;
if (recScreen) recScreen.classList.remove('landscape-right', 'landscape-left');
- const nav = document.querySelector('.bottom-nav');
+ let nav = document.querySelector('.bottom-nav');
if (nav) nav.style.display = '';
map.setBearing(0);
setTimeout(() => map.invalidateSize(), 100);
@@ -2888,7 +2897,7 @@ function renderRecordingInline(container) {
btnWrap.appendChild(lockBtn);
// Roads toggle button
- const roadsBtn = document.createElement("button");
+ let roadsBtn = document.createElement("button");
roadsBtn.className = "rec-map-btn rec-roads-toggle";
roadsBtn.textContent = "R";
let roadLayers = [];
@@ -2902,16 +2911,16 @@ function renderRecordingInline(container) {
}
roadsBtn.classList.add("active");
localStorage.setItem("baldav_show_roads", "1");
- const pts = await DB.getRoadPoints();
+ let pts = await DB.getRoadPoints();
let seg = [];
for (let i = 0; i < pts.length; i++) {
if (seg.length > 0) {
- const prev = seg[seg.length - 1];
- const dlat = (pts[i].lat - prev.lat) * 111000;
- const dlon = (pts[i].lon - prev.lon) * 111000 * Math.cos(pts[i].lat * Math.PI / 180);
+ let prev = seg[seg.length - 1];
+ let dlat = (pts[i].lat - prev.lat) * 111000;
+ let dlon = (pts[i].lon - prev.lon) * 111000 * Math.cos(pts[i].lat * Math.PI / 180);
if (dlat * dlat + dlon * dlon > 10000 || pts[i].atype !== prev.atype) {
if (seg.length >= 2) {
- const color = seg[0].atype === "w" ? "#8B5E3C" : "#2266aa";
+ let color = seg[0].atype === "w" ? "#8B5E3C" : "#2266aa";
roadLayers.push(L.polyline(seg.map(p => [p.lat, p.lon]), { color, weight: 3, opacity: 0.6 }).addTo(map));
}
seg = [];
@@ -2920,7 +2929,7 @@ function renderRecordingInline(container) {
seg.push(pts[i]);
}
if (seg.length >= 2) {
- const color = seg[0].atype === "w" ? "#8B5E3C" : "#2266aa";
+ let color = seg[0].atype === "w" ? "#8B5E3C" : "#2266aa";
roadLayers.push(L.polyline(seg.map(p => [p.lat, p.lon]), { color, weight: 3, opacity: 0.6 }).addTo(map));
}
};
@@ -2930,13 +2939,26 @@ function renderRecordingInline(container) {
function handleOrientation(e) {
if (orientMode === "off") return; // no compass, no landscape
- const heading = e.webkitCompassHeading || e.alpha || 0;
- const gamma = e.gamma || 0;
+ let heading = e.webkitCompassHeading || e.alpha || 0;
+ let beta = e.beta || 0;
+ let gamma = e.gamma || 0;
+ lastBeta = beta;
+ lastGamma = gamma;
+
+ // Gimbal lock compensation: when phone tilts past horizontal,
+ // browser flips alpha by 180° (Euler angle discontinuity).
+ // Detect by checking if phone is "face-down" and compensate.
+ if (currentLandscape !== null && Math.abs(beta) > 90) {
+ heading = heading + 180;
+ }
+ if (currentLandscape === null && Math.abs(gamma) > 90) {
+ heading = heading + 180;
+ }
// Landscape detection with hysteresis: enter at 75, exit at 25
if (orientMode === "auto") {
- const enterThreshold = 75;
- const exitThreshold = 25;
+ let enterThreshold = 75;
+ let exitThreshold = 25;
let newLandscape = currentLandscape;
if (currentLandscape === null) {
if (gamma > enterThreshold) newLandscape = 'right';
@@ -2953,26 +2975,51 @@ function renderRecordingInline(container) {
recScreen.classList.remove('landscape-right', 'landscape-left');
if (newLandscape) recScreen.classList.add('landscape-' + newLandscape);
}
- const nav = document.querySelector('.bottom-nav');
+ let nav = document.querySelector('.bottom-nav');
if (nav) nav.style.display = newLandscape ? 'none' : '';
setTimeout(() => map.invalidateSize(), 100);
}
}
+ // Debug: show all 3 axes on map overlay
+ let dbgEl = document.querySelector("#rec-orient-debug");
+ if (!dbgEl) {
+ dbgEl = document.createElement("div");
+ dbgEl.id = "rec-orient-debug";
+ dbgEl.style.cssText = "position:absolute;bottom:4px;left:4px;z-index:1001;background:rgba(0,0,0,0.7);color:#0f0;font:11px monospace;padding:4px 6px;border-radius:4px;pointer-events:none;";
+ mapEl.parentElement.appendChild(dbgEl);
+ }
+ dbgEl.textContent = `a:${Math.round(e.alpha||0)} b:${Math.round(e.beta||0)} g:${Math.round(e.gamma||0)} h:${Math.round(heading)} m:${orientMode}`;
+
+ // Smooth heading with EMA (exponential moving average) — reduces jitter from bumps
+ // Factor 0.3 = responsive but smooth (~3 samples to settle, 300ms at 10Hz)
+ let finalHeading = heading;
+ if (smoothedHeading === null) {
+ smoothedHeading = finalHeading;
+ } else {
+ let diff = finalHeading - smoothedHeading;
+ if (diff > 180) diff -= 360;
+ if (diff < -180) diff += 360;
+ smoothedHeading = (smoothedHeading + diff * 0.3) % 360;
+ if (smoothedHeading < 0) smoothedHeading += 360;
+ finalHeading = smoothedHeading;
+ }
+
// Map bearing always updates in auto and lock modes
let bearingOffset = 0;
if (currentLandscape === 'right') bearingOffset = 90;
else if (currentLandscape === 'left') bearingOffset = -90;
- map.setBearing(heading + bearingOffset);
+ map.setBearing(finalHeading + bearingOffset);
}
- const throttledOrientation = L.Util.throttle(handleOrientation, 100);
- const orientEvent = 'ondeviceorientationabsolute' in window
+
+ let throttledOrientation = L.Util.throttle(handleOrientation, 100);
+ let orientEvent = 'ondeviceorientationabsolute' in window
? 'deviceorientationabsolute' : 'deviceorientation';
window.addEventListener(orientEvent, throttledOrientation);
GPS.recording._compassCleanup = () => {
window.removeEventListener(orientEvent, throttledOrientation);
if (recScreen) recScreen.classList.remove('landscape-right', 'landscape-left');
- const nav = document.querySelector('.bottom-nav');
+ let nav = document.querySelector('.bottom-nav');
if (nav) nav.style.display = '';
};
let posMarker = null;
@@ -2980,7 +3027,7 @@ function renderRecordingInline(container) {
// State changes
GPS.recording.onStateChange = (state, p) => {
if (state === "ready") {
- const ph = document.getElementById('gps-placeholder');
+ let ph = document.getElementById('gps-placeholder');
if (ph) ph.remove();
statusEl.textContent = "GPS OK. Начните движение...";
statusEl.style.color = "#03dac6";
@@ -2995,19 +3042,19 @@ function renderRecordingInline(container) {
};
// Live point updates
- const distEl = document.querySelector("#rec-dist");
- const spdEl = document.querySelector("#rec-speed");
- const timeEl = document.querySelector("#rec-time");
- const elevEl = document.querySelector("#rec-elev");
- const statusEl = document.querySelector("#rec-status");
+ let distEl = document.querySelector("#rec-dist");
+ let spdEl = document.querySelector("#rec-speed");
+ let timeEl = document.querySelector("#rec-time");
+ let elevEl = document.querySelector("#rec-elev");
+ let statusEl = document.querySelector("#rec-status");
// Speed buffer for median
- const speedBuf = [];
+ let speedBuf = [];
GPS.recording.onPoint = (p) => {
- const ph = document.getElementById('gps-placeholder');
+ let ph = document.getElementById('gps-placeholder');
if (ph) ph.remove();
- const ll = [p.lat, p.lon];
+ let ll = [p.lat, p.lon];
borderLine.addLatLng(ll);
trailLine.addLatLng(ll);
map.panTo(ll);
@@ -3019,12 +3066,12 @@ function renderRecordingInline(container) {
posMarker = L.circleMarker(ll, { radius: 6, color: "#4285f4", fillColor: "#4285f4", fillOpacity: 1, weight: 2 }).addTo(map);
// Stats
- const pts = GPS.recording.points;
- const stats = GPS.computeStats(pts, GPS.recording.weightKg, GPS.getRecordingOpts());
+ let pts = GPS.recording.points;
+ let stats = GPS.computeStats(pts, GPS.recording.weightKg, GPS.getRecordingOpts());
if (distEl) distEl.textContent = GPS.formatDistance(stats.distance);
// Show current state
- const recState = GPS.recording.state;
+ let recState = GPS.recording.state;
if (recState === "waiting_gps") {
statusEl.textContent = "Поиск GPS...";
statusEl.style.color = "#ffb74d";
@@ -3039,15 +3086,15 @@ function renderRecordingInline(container) {
}
// Speed
- const curSpeed = (p.speed_ms != null && p.speed_ms >= 0) ? p.speed_ms * 3.6 : 0;
+ let curSpeed = (p.speed_ms != null && p.speed_ms >= 0) ? p.speed_ms * 3.6 : 0;
speedBuf.push(curSpeed);
if (speedBuf.length > 5) speedBuf.shift();
- const sorted = speedBuf.slice().sort((a, b) => a - b);
- const median = sorted[Math.floor(sorted.length / 2)] || 0;
+ let sorted = speedBuf.slice().sort((a, b) => a - b);
+ let median = sorted[Math.floor(sorted.length / 2)] || 0;
if (spdEl) spdEl.textContent = median.toFixed(1);
// Elevation
- const alt = p.altitude;
+ let alt = p.altitude;
if (alt != null && elevEl) elevEl.textContent = Math.round(alt) + " м";
// Pause indicator
@@ -3062,56 +3109,56 @@ function renderRecordingInline(container) {
// HR update from BLE
GPS.recording.onHrUpdate = (reading) => {
- const row = document.querySelector("#rec-hr-row");
+ let row = document.querySelector("#rec-hr-row");
if (!row) return;
row.style.display = "flex";
- const hrEl = document.querySelector("#rec-hr-val");
+ let hrEl = document.querySelector("#rec-hr-val");
if (hrEl) {
hrEl.textContent = reading.hr;
hrEl.className = "rec-hr-val " + hrZoneClass(reading.hr);
}
- const spo2El = document.querySelector("#rec-spo2");
+ let spo2El = document.querySelector("#rec-spo2");
if (spo2El) spo2El.textContent = reading.spo2 ? `SpO2: ${reading.spo2}%` : "";
- const bpEl = document.querySelector("#rec-bp");
+ let bpEl = document.querySelector("#rec-bp");
if (bpEl) bpEl.textContent = (reading.bp_sys && reading.bp_dia) ? `${reading.bp_sys}/${reading.bp_dia}` : "";
};
// Timer — excludes pauses, starts only when recording
- const timer = setInterval(() => {
+ let timer = setInterval(() => {
if (!GPS.recording.active) { clearInterval(timer); return; }
if (!GPS.recording.startTime) return; // not started yet
- const totalMs = Date.now() - GPS.recording.startTime.getTime();
- const activeMs = totalMs - GPS.recording.totalPauseMs -
+ let totalMs = Date.now() - GPS.recording.startTime.getTime();
+ let activeMs = totalMs - GPS.recording.totalPauseMs -
(GPS.recording.pauseStart ? Date.now() - GPS.recording.pauseStart.getTime() : 0);
- const sec = Math.max(0, Math.round(activeMs / 1000));
+ let sec = Math.max(0, Math.round(activeMs / 1000));
if (timeEl) timeEl.textContent = GPS.formatDuration(sec);
// Update clock
- const clockEl = document.querySelector("#rec-clock");
+ let clockEl = document.querySelector("#rec-clock");
if (clockEl) clockEl.textContent = new Date().toTimeString().slice(0, 5);
}, 1000);
// Fetch temperature — try multiple APIs (mobile internet blocks foreign services)
async function fetchTemperature() {
- const pos = GPS.recording.lastMovingPoint || { lat: parseFloat(state.profile.last_lat) || 47.42, lon: parseFloat(state.profile.last_lon) || 40.08 };
- const lat = pos.lat.toFixed(2), lon = pos.lon.toFixed(2);
- const invoke = window.__TAURI__.core.invoke;
- const apis = [
+ let pos = GPS.recording.lastMovingPoint || { lat: parseFloat(state.profile.last_lat) || 47.42, lon: parseFloat(state.profile.last_lon) || 40.08 };
+ let lat = pos.lat.toFixed(2), lon = pos.lon.toFixed(2);
+ let invoke = window.__TAURI__.core.invoke;
+ let apis = [
// Yandex Pogoda — works on Russian mobile internet, scrape HTML
{ url: `https://yandex.ru/pogoda/?lat=${lat}&lon=${lon}`,
- parse: r => { const m = r.match(/FactTemperature_value[^>]*>(-?\d+)/); return m ? parseInt(m[1]) : null; } },
+ parse: r => { let m = r.match(/FactTemperature_value[^>]*>(-?\d+)/); return m ? parseInt(m[1]) : null; } },
// open-meteo — free API, may be blocked on mobile
{ url: `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m`,
parse: r => { try { return JSON.parse(r).current?.temperature_2m; } catch { return null; } } },
// wttr.in — free, may be blocked on mobile
{ url: `https://wttr.in/${lat},${lon}?format=%t`,
- parse: r => { const n = parseFloat(r); return isNaN(n) ? null : n; } },
+ parse: r => { let n = parseFloat(r); return isNaN(n) ? null : n; } },
];
- for (const api of apis) {
+ for (let api of apis) {
try {
- const resp = await invoke("http_request", { url: api.url, method: "GET", body: null });
- const temp = api.parse(resp);
+ let resp = await invoke("http_request", { url: api.url, method: "GET", body: null });
+ let temp = api.parse(resp);
if (temp != null && !isNaN(temp)) {
- const el = document.querySelector("#rec-temp");
+ let el = document.querySelector("#rec-temp");
if (el) el.textContent = Math.round(temp) + "°";
return;
}
@@ -3119,7 +3166,7 @@ function renderRecordingInline(container) {
}
}
fetchTemperature();
- const weatherTimer = setInterval(fetchTemperature, 15 * 60 * 1000);
+ let weatherTimer = setInterval(fetchTemperature, 15 * 60 * 1000);
// Pause button
container.querySelector("#rec-pause").onclick = () => {
@@ -3146,10 +3193,10 @@ function renderRecordingInline(container) {
// Restore trail if already recording — sort by seq/ts to avoid jumps
if (GPS.recording.points.length > 0) {
- const sorted = GPS.recording.points.slice().sort((a, b) => {
+ let sorted = GPS.recording.points.slice().sort((a, b) => {
return new Date(a.ts) - new Date(b.ts);
});
- const coords = sorted.map(p => [p.lat, p.lon]);
+ let coords = sorted.map(p => [p.lat, p.lon]);
borderLine.setLatLngs(coords);
trailLine.setLatLngs(coords);
map.fitBounds(trailLine.getBounds().pad(0.1));
@@ -3158,28 +3205,28 @@ function renderRecordingInline(container) {
// Batch update callback — called after poll reads multiple points at once.
// Rebuilds polyline from all sorted points instead of adding one-by-one.
GPS.recording.onBatchUpdate = () => {
- const sorted = GPS.recording.points.slice().sort((a, b) => {
+ let sorted = GPS.recording.points.slice().sort((a, b) => {
return new Date(a.ts) - new Date(b.ts);
});
- const coords = sorted.map(p => [p.lat, p.lon]);
+ let coords = sorted.map(p => [p.lat, p.lon]);
borderLine.setLatLngs(coords);
trailLine.setLatLngs(coords);
if (coords.length) {
- const last = coords[coords.length - 1];
+ let last = coords[coords.length - 1];
map.panTo(last);
if (posMarker) map.removeLayer(posMarker);
posMarker = L.circleMarker(last, { radius: 6, color: "#4285f4", fillColor: "#4285f4", fillOpacity: 1, weight: 2 }).addTo(map);
}
- const stats = GPS.computeStats(GPS.recording.points, GPS.recording.weightKg, GPS.getRecordingOpts());
+ let stats = GPS.computeStats(GPS.recording.points, GPS.recording.weightKg, GPS.getRecordingOpts());
if (distEl) distEl.textContent = GPS.formatDistance(stats.distance);
};
- }, 200);
+
}
function setupLiveUpdates(container) {
- const distEl = container.querySelector("#gs-dist");
- const spdEl = container.querySelector("#gs-speed");
- const timeEl = container.querySelector("#gs-time");
+ let distEl = container.querySelector("#gs-dist");
+ let spdEl = container.querySelector("#gs-speed");
+ let timeEl = container.querySelector("#gs-time");
let timer = null;
GPS.recording.onPoint = (p) => {
@@ -3187,22 +3234,22 @@ function setupLiveUpdates(container) {
gpsTrailLine.addLatLng([p.lat, p.lon]);
gpsMap.panTo([p.lat, p.lon]);
- const pts = GPS.recording.points;
- const stats = GPS.computeStats(pts, GPS.recording.weightKg, GPS.getRecordingOpts());
+ let pts = GPS.recording.points;
+ let stats = GPS.computeStats(pts, GPS.recording.weightKg, GPS.getRecordingOpts());
if (distEl) distEl.textContent = GPS.formatDistance(stats.distance);
if (spdEl) {
- const curSpd = p.speed_ms != null ? (p.speed_ms * 3.6).toFixed(1) : stats.avgSpeed;
+ let curSpd = p.speed_ms != null ? (p.speed_ms * 3.6).toFixed(1) : stats.avgSpeed;
spdEl.textContent = curSpd + " км/ч";
}
};
GPS.recording.onBatchUpdate = () => {
if (!gpsTrailLine || !gpsMap) return;
- const sorted = GPS.recording.points.slice().sort((a, b) => new Date(a.ts) - new Date(b.ts));
- const coords = sorted.map(p => [p.lat, p.lon]);
+ let sorted = GPS.recording.points.slice().sort((a, b) => new Date(a.ts) - new Date(b.ts));
+ let coords = sorted.map(p => [p.lat, p.lon]);
gpsTrailLine.setLatLngs(coords);
if (coords.length) gpsMap.panTo(coords[coords.length - 1]);
- const stats = GPS.computeStats(GPS.recording.points, GPS.recording.weightKg, GPS.getRecordingOpts());
+ let stats = GPS.computeStats(GPS.recording.points, GPS.recording.weightKg, GPS.getRecordingOpts());
if (distEl) distEl.textContent = GPS.formatDistance(stats.distance);
if (spdEl) spdEl.textContent = stats.avgSpeed + " км/ч";
};
@@ -3210,7 +3257,7 @@ function setupLiveUpdates(container) {
// Timer for duration display
timer = setInterval(() => {
if (!GPS.recording.active) { clearInterval(timer); return; }
- const sec = Math.round((Date.now() - GPS.recording.startTime.getTime()) / 1000);
+ let sec = Math.round((Date.now() - GPS.recording.startTime.getTime()) / 1000);
if (timeEl) timeEl.textContent = GPS.formatDuration(sec);
}, 1000);
}
@@ -3219,7 +3266,7 @@ function setupLiveUpdates(container) {
// Map download modal — pick region + zoom range
// ---------------------------------------------------------------------------
-const MAP_REGIONS = [
+let MAP_REGIONS = [
{ name: "Текущий вид карты", bounds: null },
{ name: "--- Города ---", bounds: null, disabled: true },
{ name: "Москва", bounds: [[55.55, 37.35], [55.95, 37.85]] },
@@ -3238,7 +3285,7 @@ const MAP_REGIONS = [
];
function showMapDownloadModal() {
- const regionOpts = MAP_REGIONS.map((r, i) =>
+ let regionOpts = MAP_REGIONS.map((r, i) =>
r.disabled
? `
${r.name.replace(/---? ?/g, "")}
`
: `
${r.name}
`
@@ -3260,12 +3307,12 @@ function showMapDownloadModal() {
`,
async overlay => {
- const sel = overlay.querySelector(".supp-pick-item.selected");
+ let sel = overlay.querySelector(".supp-pick-item.selected");
if (!sel) return false;
- const idx = parseInt(sel.dataset.idx);
- const region = MAP_REGIONS[idx];
- const zMin = parseInt(overlay.querySelector("#m-zmin").value);
- const zMax = parseInt(overlay.querySelector("#m-zmax").value);
+ let idx = parseInt(sel.dataset.idx);
+ let region = MAP_REGIONS[idx];
+ let zMin = parseInt(overlay.querySelector("#m-zmin").value);
+ let zMax = parseInt(overlay.querySelector("#m-zmax").value);
let bounds;
if (region.bounds) {
bounds = L.latLngBounds(region.bounds);
@@ -3273,8 +3320,8 @@ function showMapDownloadModal() {
bounds = gpsMap.getBounds();
} else return false;
- const total = TileCache.countTilesForArea(bounds, zMin, zMax);
- const okBtn = overlay.querySelector("#modal-ok");
+ let total = TileCache.countTilesForArea(bounds, zMin, zMax);
+ let okBtn = overlay.querySelector("#modal-ok");
okBtn.textContent = `0 / ${total}`;
okBtn.disabled = true;
await TileCache.downloadArea(bounds, zMin, zMax, (done, tot) => {
@@ -3284,8 +3331,8 @@ function showMapDownloadModal() {
okBtn.disabled = false;
});
- const overlay = [...document.querySelectorAll(".modal-overlay")].pop();
- const countEl = overlay.querySelector("#m-tile-count");
+ let overlay = [...document.querySelectorAll(".modal-overlay")].pop();
+ let countEl = overlay.querySelector("#m-tile-count");
// Selection
overlay.querySelectorAll(".supp-pick-item").forEach(item => {
@@ -3297,17 +3344,17 @@ function showMapDownloadModal() {
});
function updateTileCount() {
- const sel = overlay.querySelector(".supp-pick-item.selected");
+ let sel = overlay.querySelector(".supp-pick-item.selected");
if (!sel) return;
- const idx = parseInt(sel.dataset.idx);
- const region = MAP_REGIONS[idx];
- const zMin = parseInt(overlay.querySelector("#m-zmin").value);
- const zMax = parseInt(overlay.querySelector("#m-zmax").value);
+ let idx = parseInt(sel.dataset.idx);
+ let region = MAP_REGIONS[idx];
+ let zMin = parseInt(overlay.querySelector("#m-zmin").value);
+ let zMax = parseInt(overlay.querySelector("#m-zmax").value);
let bounds;
if (region.bounds) bounds = L.latLngBounds(region.bounds);
else if (gpsMap) bounds = gpsMap.getBounds();
else return;
- const total = TileCache.countTilesForArea(bounds, zMin, zMax);
+ let total = TileCache.countTilesForArea(bounds, zMin, zMax);
countEl.textContent = `~${total} тайлов`;
}
@@ -3321,20 +3368,20 @@ function showMapDownloadModal() {
let _sessionDetailRender = 0;
async function showSessionDetail(sessionId) {
- const renderToken = ++_sessionDetailRender;
+ let renderToken = ++_sessionDetailRender;
try {
console.log("[SD] loading session", sessionId);
- const session = await DB.getGpsSession(sessionId);
+ let session = await DB.getGpsSession(sessionId);
if (!session) { console.error("[SD] session not found"); return; }
console.log("[SD] loading points");
- const rawPoints = await DB.getGpsPoints(sessionId);
+ let rawPoints = await DB.getGpsPoints(sessionId);
console.log("[SD] got", rawPoints.length, "raw points");
- const points = GPS.filterNoise(rawPoints, session.activity_type);
+ let points = GPS.filterNoise(rawPoints, session.activity_type);
console.log("[SD] filtered to", points.length, "points");
// Use stored session stats if available (computed correctly at import time),
// fallback to recomputing from points
- const storedStats = session.total_distance_m != null;
- const stats = storedStats ? {
+ let storedStats = session.total_distance_m != null;
+ let stats = storedStats ? {
distance: Math.round(session.total_distance_m || 0),
avgSpeed: session.avg_speed_kmh || 0,
maxSpeed: session.max_speed_kmh || 0,
@@ -3349,25 +3396,25 @@ async function showSessionDetail(sessionId) {
}
// Always compute segments from points (needed for the Segments section)
if (!stats.segments && points.length >= 3) {
- const fullStats = GPS.computeStats(points, state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80);
+ let fullStats = GPS.computeStats(points, state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80);
stats.segments = fullStats.segments;
}
// Speed: prefer device-reported speed (Doppler, smooth), fallback to position-based
- const rawSpeeds = [];
+ let rawSpeeds = [];
for (let i = 1; i < points.length; i++) {
- const devSpd = points[i].speed_ms;
+ let devSpd = points[i].speed_ms;
if (devSpd != null && devSpd >= 0) {
rawSpeeds.push(devSpd * 3.6);
} else {
- const d = GPS.distance(points[i-1].lat, points[i-1].lon, points[i].lat, points[i].lon);
- const dt = (new Date(points[i].ts) - new Date(points[i-1].ts)) / 1000;
+ let d = GPS.distance(points[i-1].lat, points[i-1].lon, points[i].lat, points[i].lon);
+ let dt = (new Date(points[i].ts) - new Date(points[i-1].ts)) / 1000;
rawSpeeds.push(dt > 0 ? (d / dt) * 3.6 : 0);
}
}
// 31-point moving average — kills GPS jitter from phone-in-pocket
- const HALF_WIN = 15;
- const smoothed = rawSpeeds.map((_, i) => {
+ let HALF_WIN = 15;
+ let smoothed = rawSpeeds.map((_, i) => {
let sum = 0, count = 0;
for (let j = Math.max(0, i - HALF_WIN); j <= Math.min(rawSpeeds.length - 1, i + HALF_WIN); j++) {
sum += rawSpeeds[j]; count++;
@@ -3376,26 +3423,26 @@ async function showSessionDetail(sessionId) {
});
// maxSpeed from smoothed graph (not raw)
- const graphMaxSpeed = smoothed.length ? Math.max(...smoothed) : 0;
+ let graphMaxSpeed = smoothed.length ? Math.max(...smoothed) : 0;
// Elevation data
- const elevations = GPS.filterAndSmoothAlt(points.map(p => p.altitude_gps_m));
- const minElev = elevations.length ? Math.round(Math.min(...elevations)) : null;
- const maxElev = elevations.length ? Math.round(Math.max(...elevations)) : null;
+ let elevations = GPS.filterAndSmoothAlt(points.map(p => p.altitude_gps_m));
+ let minElev = elevations.length ? Math.round(Math.min(...elevations)) : null;
+ let maxElev = elevations.length ? Math.round(Math.max(...elevations)) : null;
// Time labels (minutes from start)
- const t0 = points.length ? new Date(points[0].ts).getTime() : 0;
- const timeLabels = points.slice(1).map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1));
+ let t0 = points.length ? new Date(points[0].ts).getTime() : 0;
+ let timeLabels = points.slice(1).map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1));
- const actNames = { bike: "Велосипед", walk: "Ходьба", run: "Бег", other: "Другое" };
- const actName = actNames[session.activity_type] || session.activity_type || "Тренировка";
- const duration = session.end_time && session.start_time
+ let actNames = { bike: "Велосипед", walk: "Ходьба", run: "Бег", other: "Другое" };
+ let actName = actNames[session.activity_type] || session.activity_type || "Тренировка";
+ let duration = session.end_time && session.start_time
? GPS.formatDuration(stats.duration)
: "—";
- const tempo = stats.duration > 0 && stats.distance > 0
+ let tempo = stats.duration > 0 && stats.distance > 0
? GPS.formatDuration(Math.round(stats.duration / (stats.distance / 1000))) + " мин/км"
: "—";
- const kcalMin = stats.duration > 0 ? (stats.kcal / (stats.duration / 60)).toFixed(1) : "—";
+ let kcalMin = stats.duration > 0 ? (stats.kcal / (stats.duration / 60)).toFixed(1) : "—";
showModal(`
${actName}
@@ -3458,12 +3505,12 @@ async function showSessionDetail(sessionId) {