From ff12646844e041ef11b5219915d74c0dbc1aff47 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Wed, 13 May 2026 17:53:19 +0300 Subject: [PATCH] GPS track rendering + offline maps + UI improvements Track rendering: seq-based ordering matching FitoTrack v15.6 (insertion order, not relativeTime sort which scatters AGPS points). Colored time-gradient points with dark contour on track map. Offline maps: tile cache with online/offline toggle in settings, download script (tools/download_tiles.js) with parallel curl. UI: removed useless map from GPS screen, click-to-zoom on detail map, locate-me button, zoom level display, desktop layout improvements. --- frontend/css/styles.css | 19 ++++---- frontend/js/main.js | 97 +++++++++++++++++++++++++--------------- frontend/js/tilecache.js | 25 +++++++++-- 3 files changed, 91 insertions(+), 50 deletions(-) diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 5e192c7..9f23112 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -879,20 +879,12 @@ body { z-index: 1; } -.gps-controls { +.gps-header { padding: 10px 12px; background: var(--surface); border-bottom: 1px solid var(--border); } -.gps-stats { - display: flex; - justify-content: space-around; - padding: 6px 0 10px; - font-size: 16px; - font-weight: 600; -} - .gps-btns { display: flex; gap: 8px; @@ -1040,6 +1032,15 @@ body { font-size: 16px; } +.zoom-display { + background: rgba(0,0,0,0.6); + color: #fff; + font-size: 13px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; +} + .gps-download-status { text-align: center; font-size: 12px; diff --git a/frontend/js/main.js b/frontend/js/main.js index 62890b9..80aa14e 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -674,13 +674,7 @@ async function renderGpsScreen(container) { container.innerHTML = `
-
-
-
- 0 м - 0 км/ч - 0:00 -
+
+
- +
`, async overlay => { @@ -1069,6 +1037,7 @@ async function showSessionDetail(sessionId) { L.Icon.Default.imagePath = "lib/"; const map = L.map(mapEl, { attributionControl: false, zoomControl: false }); + addZoomDisplay(map); const CachedLayer = TileCache.createLayer(); new CachedLayer({ maxZoom: 19 }).addTo(map); @@ -1142,6 +1111,33 @@ async function showSessionDetail(sessionId) { return fsDiv; }; fsBtn.addTo(map); + + // Locate me button + const locBtn = L.control({ position: "topleft" }); + locBtn.onAdd = () => { + const div = L.DomUtil.create("div", "sd-map-fs-btn"); + div.innerHTML = "◎"; + div.title = "Моё местоположение"; + div.onclick = (e) => { + e.stopPropagation(); + navigator.geolocation?.getCurrentPosition( + pos => { + const ll = [pos.coords.latitude, pos.coords.longitude]; + map.setView(ll, 15); + L.circleMarker(ll, { radius: 8, color: "#4285f4", fillColor: "#4285f4", fillOpacity: 0.8, weight: 2 }).addTo(map); + }, + err => alert("GPS: " + err.message), + { enableHighAccuracy: true, timeout: 10000 } + ); + }; + return div; + }; + locBtn.addTo(map); + + // Click map to toggle fullscreen (same as button) + map.on("click", () => { + if (!mapEl.classList.contains("sd-map-full")) toggleMapFs(); + }); }, 200); // Render speed chart @@ -1444,18 +1440,34 @@ async function renderSettings(container) {
+
+ +
+ + +
+ Оффлайн: тайлы только из кэша, интернет не используется +
`; let selectedSex = p.sex ?? "m"; - container.querySelectorAll(".type-chip").forEach(btn => { + container.querySelectorAll("[data-sex]").forEach(btn => { btn.onclick = () => { selectedSex = btn.dataset.sex; - container.querySelectorAll(".type-chip").forEach(b => b.classList.remove("selected")); + container.querySelectorAll("[data-sex]").forEach(b => b.classList.remove("selected")); btn.classList.add("selected"); }; }); + container.querySelectorAll("[data-map]").forEach(btn => { + btn.onclick = () => { + container.querySelectorAll("[data-map]").forEach(b => b.classList.remove("selected")); + btn.classList.add("selected"); + TileCache.setOfflineMode(btn.dataset.map === "offline"); + }; + }); + container.querySelector("#save-profile").onclick = async () => { const height = parseInt(container.querySelector("#s-height").value); const year = parseInt(container.querySelector("#s-year").value); @@ -1564,6 +1576,17 @@ function formatDate(iso) { .toLocaleDateString("ru-RU", { weekday: "long", day: "numeric", month: "long" }); } +function addZoomDisplay(map) { + const ctrl = L.control({ position: "bottomright" }); + ctrl.onAdd = () => { + const div = L.DomUtil.create("div", "zoom-display"); + div.textContent = map.getZoom(); + map.on("zoomend", () => { div.textContent = map.getZoom(); }); + return div; + }; + ctrl.addTo(map); +} + function pickFile(accept, callback, asBinary) { const input = document.createElement("input"); input.type = "file"; diff --git a/frontend/js/tilecache.js b/frontend/js/tilecache.js index 6020e89..07100d1 100644 --- a/frontend/js/tilecache.js +++ b/frontend/js/tilecache.js @@ -95,7 +95,7 @@ TileCache.downloadArea = async function(bounds, zoomMin, zoomMax, onProgress) { } let done = 0; - const PARALLEL = 4; + const PARALLEL = 12; async function downloadOne(tile) { const key = `${tile.z}/${tile.x}/${tile.y}`; @@ -135,11 +135,13 @@ TileCache.createLayer = function() { if (blob) { tile.src = URL.createObjectURL(blob); done(null, tile); + } else if (TileCache.offlineMode) { + // Offline: show empty tile + done(null, tile); } else { - // Fallback to network + // Fetch from network tile.crossOrigin = ""; tile.src = `https://tile.openstreetmap.org/${key}.png`; - // Cache after download tile.onload = () => { fetch(tile.src).then(r => r.blob()).then(b => putTile(key, b)).catch(() => {}); done(null, tile); @@ -147,6 +149,7 @@ TileCache.createLayer = function() { tile.onerror = () => done(new Error("tile failed"), tile); } }).catch(() => { + if (TileCache.offlineMode) { done(null, tile); return; } tile.src = `https://tile.openstreetmap.org/${key}.png`; tile.onload = () => done(null, tile); tile.onerror = () => done(new Error("tile failed"), tile); @@ -157,5 +160,19 @@ TileCache.createLayer = function() { }); }; +// Offline mode — when true, tiles only served from cache, never fetched from network +TileCache.offlineMode = false; + +TileCache.setOfflineMode = function(val) { + TileCache.offlineMode = !!val; + localStorage.setItem("baldav_offline_maps", val ? "1" : "0"); +}; + +TileCache.loadOfflineMode = function() { + TileCache.offlineMode = localStorage.getItem("baldav_offline_maps") === "1"; +}; + +TileCache.loadOfflineMode(); + window.TileCache = TileCache; -console.log("[tilecache.js] window.TileCache ready"); +console.log("[tilecache.js] window.TileCache ready, offline:", TileCache.offlineMode);