diff --git a/PROJECT_MAP.md b/PROJECT_MAP.md index 89beeee..0f8352e 100644 --- a/PROJECT_MAP.md +++ b/PROJECT_MAP.md @@ -22,7 +22,7 @@ | Файл | Строк | Назначение | |------|-------|------------| | `js/main.js` | ~5900 | Экраны, навигация, модалки, графики, нутриенты, HR UI, компас (EMA + gimbal lock), ландшафт (3 режима), дороги. `appConfirm`/`appPrompt` (in-app диалоги вместо нативных, в стеке окон). Страница «Сохранённые маршруты», оверлей маршрута на записи и просмотре (`whenMapReady` — await готовности карты, не таймаут). Панель де-джиттера на детали трека (ползунки окно/итерации, живое превью, сохранить/откат, `reopenDetail` — переоткрытие детали без наложения оверлеев). `showRouteDetail` — предпросмотр маршрута (карта+профиль высоты+тап-высоты+фуллскрин), `showRouteEditor` — создание/правка маршрута (тап карты=точка в конец, тап линии=вставка в сегмент, тащить=двигать, тап точки=удалить; `makeMarker` возвращает маркер для push/splice). `appConfirm`/`appPrompt`/`appAlert` — in-app диалоги (нативных `alert/prompt/confirm` не осталось; ` - ${esc(r.name)} ${r.pts} тчк${r.hasEle ? " · ⛰" : ""} + ${esc(r.name)} ${r.wpts != null ? r.wpts + " тчк" : r.pts + " тчк"}${r.hasEle ? " · ⛰" : ""} @@ -3486,19 +3488,23 @@ async function showRouteDetail(container, routeId) { async function showRouteEditor(container, routeId) { let editing = routeId != null; let route = editing ? await DB.getRoute(routeId) : null; - let points = (editing && route) ? route.points.map(p => ({ lat: p.lat, lon: p.lon })) : []; + // Editable waypoints. Planner-built routes store them separately (5-6 pts) + per-leg mode; + // imported / legacy routes only have dense geometry → load that as waypoints (legs straight). + let wpSaved = (editing && route && route.waypoints && route.waypoints.waypoints) ? route.waypoints : null; + let points = wpSaved + ? wpSaved.waypoints.map(p => ({ lat: p.lat, lon: p.lon })) + : ((editing && route) ? route.points.map(p => ({ lat: p.lat, lon: p.lon })) : []); let color = (editing && route) ? (route.color || "#e53935") : "#e53935"; let markers = []; - // Per-leg road routing: each leg between consecutive waypoints is independently - // "road" (BRouter geometry cached in legGeom[i]) or "straight". Default = road; tap a - // leg to toggle just that leg; a road leg that can't be routed falls back to straight - // (so leading to where there is no road still draws a straight line, silently). - let legLines = []; // L.polyline per leg (parallel to legs) + // Per-leg road routing: each leg between consecutive waypoints is independently "road" + // (BRouter geometry, with elevation, cached in legGeom[i]) or "straight". Default = road. + let hitLines = []; // transparent fat polyline per leg — the tap/long-press target + let displayLayer = null; // the visible route (casing+slope+thin line) via drawRoutePolyline let legMode = []; // "road" | "straight", length = points.length - 1 - let legGeom = []; // routed [{lat,lon}] per road leg, or null - let defaultMode = "road"; // applied to new legs and by the global toggle + let legGeom = []; // routed [{lat,lon,ele}] per road leg, or null + let defaultMode = "road"; let routeTimer = null; - let dirty = new Set(); // leg indices awaiting a (debounced) route + let dirty = new Set(); container.innerHTML = `
@@ -3507,12 +3513,14 @@ async function showRouteEditor(container, routeId) {
0 мДистанция
0Точек
-
- тап карты — точка · тап отрезка — дорога/прямая · долгий тап — вставить · тап точки — удалить - - - - +
+
тап карты — точка · тап линии — вставить точку · долгий тап — дорога/прямая · тап точки — удалить
+
+ + + +
+
`; @@ -3527,8 +3535,8 @@ async function showRouteEditor(container, routeId) { let ptsEl = document.querySelector("#re-pts"); let dotIcon = L.divIcon({ className: "re-dot-wrap", - html: `
`, - iconSize: [16, 16], iconAnchor: [8, 8], + html: `
`, + iconSize: [18, 18], iconAnchor: [9, 9], }); function normalizeLegs() { @@ -3536,19 +3544,19 @@ async function showRouteEditor(container, routeId) { while (legMode.length < need) { legMode.push(defaultMode); legGeom.push(null); } legMode.length = need; legGeom.length = need; } - // Geometry drawn/saved for a leg: routed pts for a routed road leg, else the straight - // chord (also the fallback when a road leg couldn't be routed). + // Drawn/saved geometry for a leg: routed pts for a routed road leg (carry ele), else the + // straight chord (also the fallback when a road leg couldn't be routed). function legPts(i) { if (legMode[i] === "road" && legGeom[i] && legGeom[i].length >= 2) return legGeom[i]; return [points[i], points[i + 1]]; } - // Full dense geometry = concat of every leg (shared junction point not duplicated). + // Full dense geometry = concat of every leg (shared junction not duplicated); keeps ele. function fullGeom() { if (points.length < 2) return points.map(p => ({ lat: p.lat, lon: p.lon })); let out = [{ lat: points[0].lat, lon: points[0].lon }]; for (let i = 0; i < points.length - 1; i++) { let lp = legPts(i); - for (let k = 1; k < lp.length; k++) out.push({ lat: lp[k].lat, lon: lp[k].lon }); + for (let k = 1; k < lp.length; k++) out.push({ lat: lp[k].lat, lon: lp[k].lon, ele: lp[k].ele }); } return out; } @@ -3558,22 +3566,29 @@ async function showRouteEditor(container, routeId) { distEl.textContent = GPS.formatDistance(dist); ptsEl.textContent = String(points.length); } - function drawLegs() { - legLines.forEach(l => map.removeLayer(l)); legLines = []; - for (let i = 0; i < points.length - 1; i++) { - let routed = legMode[i] === "road" && legGeom[i] && legGeom[i].length >= 2; - let line = L.polyline(legPts(i).map(p => [p.lat, p.lon]), { - color, weight: 4, interactive: true, - dashArray: legMode[i] === "straight" ? "6,8" : null, - opacity: legMode[i] === "straight" ? 0.9 : (routed ? 1 : 0.5), - }).addTo(map); - (function (idx) { - line.on("click", (e) => { L.DomEvent.stop(e); toggleLeg(idx); }); // tap → road/straight - line.on("contextmenu", (e) => { L.DomEvent.stop(e); insertInLeg(idx, e.latlng); }); // long-press → insert - })(i); - legLines.push(line); + // Visible route: casing (slope/elev colour) + thin identity line, like the detail view. + function drawDisplay() { + if (displayLayer) { map.removeLayer(displayLayer); displayLayer = null; } + if (points.length >= 2) { + let mode = localStorage.getItem("baldav_route_color_mode") || "slope"; + displayLayer = drawRoutePolyline(map, fullGeom(), color, mode); } } + // Fat transparent hit target per leg (tap = insert, long-press = toggle road/straight). + // pointer-events:stroke (CSS .re-hit) makes the invisible fat stroke clickable. + function drawHitLines() { + hitLines.forEach(l => map.removeLayer(l)); hitLines = []; + for (let i = 0; i < points.length - 1; i++) { + let line = L.polyline(legPts(i).map(p => [p.lat, p.lon]), + { color: "#000", weight: 26, opacity: 0, interactive: true, className: "re-hit" }).addTo(map); + (function (idx) { + line.on("click", (e) => { L.DomEvent.stop(e); insertInLeg(idx, e.latlng); }); // tap → insert + line.on("contextmenu", (e) => { L.DomEvent.stop(e); toggleLeg(idx); }); // long-press → road/straight + })(i); + hitLines.push(line); + } + } + function redraw() { drawDisplay(); drawHitLines(); } function markDirty(i) { if (i >= 0 && i < points.length - 1) dirty.add(i); } function scheduleRoute() { clearTimeout(routeTimer); routeTimer = setTimeout(routeDirty, 450); } async function routeDirty() { @@ -3583,18 +3598,18 @@ async function showRouteEditor(container, routeId) { try { let gpx = await BRouter.route([[points[i].lon, points[i].lat], [points[i+1].lon, points[i+1].lat]], "trekking"); let rp = (GPS.parseRouteGPX(gpx) || {}).points || []; - legGeom[i] = (rp.length >= 2) ? rp.map(p => ({ lat: p.lat, lon: p.lon })) : null; + legGeom[i] = (rp.length >= 2) ? rp.map(p => ({ lat: p.lat, lon: p.lon, ele: p.ele })) : null; // keep ele for slope } catch (e) { legGeom[i] = null; } // silent fallback to straight (no road / no segments) } - drawLegs(); updateStats(); + redraw(); updateStats(); } function toggleLeg(i) { if (i < 0 || i >= points.length - 1) return; legMode[i] = (legMode[i] === "road") ? "straight" : "road"; if (legMode[i] === "road") { legGeom[i] = null; markDirty(i); scheduleRoute(); } - drawLegs(); updateStats(); + redraw(); updateStats(); } - function afterEdit() { normalizeLegs(); drawLegs(); updateStats(); scheduleRoute(); } + function afterEdit() { normalizeLegs(); redraw(); updateStats(); scheduleRoute(); } function makeMarker(p) { let m = L.marker([p.lat, p.lon], { draggable: true, icon: dotIcon }); @@ -3603,7 +3618,7 @@ async function showRouteEditor(container, routeId) { points[idx] = { lat: m.getLatLng().lat, lon: m.getLatLng().lng }; if (idx - 1 >= 0) legGeom[idx - 1] = null; if (idx < points.length - 1) legGeom[idx] = null; - drawLegs(); updateStats(); + redraw(); updateStats(); }); m.on("dragend", () => { let idx = markers.indexOf(m); if (idx < 0) return; @@ -3640,13 +3655,54 @@ async function showRouteEditor(container, routeId) { afterEdit(); } - // Existing route: load its saved dense points as waypoints, legs straight (reproduce - // the saved shape as-is); legs added later default to road. + // --- Map controls, reused from the track/detail views --- + addDarkMapControl(map); // day/night tiles + // Slope <-> elevation colour toggle (shared preference); redraws the route. + let colorCtl = L.control({ position: "topleft" }); + colorCtl.onAdd = () => { + let d = L.DomUtil.create("div", "sd-map-fs-btn"); + d.innerHTML = (localStorage.getItem("baldav_route_color_mode") || "slope") === "slope" ? "∡" : "⛰"; + d.title = "Раскраска маршрута: уклон / высота"; + L.DomEvent.disableClickPropagation(d); + d.onclick = (e) => { + e.stopPropagation(); + let nm = (localStorage.getItem("baldav_route_color_mode") || "slope") === "slope" ? "elev" : "slope"; + localStorage.setItem("baldav_route_color_mode", nm); + d.innerHTML = nm === "slope" ? "∡" : "⛰"; + drawDisplay(); + }; + return d; + }; + colorCtl.addTo(map); + // Locate me + let locCtl = L.control({ position: "topleft" }); + locCtl.onAdd = () => { + let d = L.DomUtil.create("div", "sd-map-fs-btn"); + d.innerHTML = "◎"; + d.title = "Моё местоположение"; + L.DomEvent.disableClickPropagation(d); + d.onclick = (e) => { + e.stopPropagation(); + navigator.geolocation?.getCurrentPosition( + pos => map.setView([pos.coords.latitude, pos.coords.longitude], 15), + err => console.error("GPS error:", err.message), { enableHighAccuracy: true, timeout: 10000 }); + }; + return d; + }; + locCtl.addTo(map); + + // Existing route: load waypoints. Planner-built → its saved leg modes (re-route road legs + // to get fresh geometry + elevations). Imported/legacy dense → all legs straight. if (points.length) { points.forEach(p => markers.push(makeMarker(p))); - for (let i = 0; i < points.length - 1; i++) { legMode[i] = "straight"; legGeom[i] = null; } - normalizeLegs(); drawLegs(); updateStats(); + for (let i = 0; i < points.length - 1; i++) { + legMode[i] = (wpSaved && wpSaved.legMode && wpSaved.legMode[i]) ? wpSaved.legMode[i] : (wpSaved ? "road" : "straight"); + legGeom[i] = null; + if (legMode[i] === "road") markDirty(i); + } + normalizeLegs(); redraw(); updateStats(); map.fitBounds(L.latLngBounds(points.map(p => [p.lat, p.lon])).pad(0.1)); + if (dirty.size) scheduleRoute(); } else if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(pos => map.setView([pos.coords.latitude, pos.coords.longitude], 15), () => {}, { enableHighAccuracy: false, timeout: 1000, maximumAge: 5000 }); @@ -3669,23 +3725,26 @@ async function showRouteEditor(container, routeId) { defaultMode = (defaultMode === "road") ? "straight" : "road"; brBtn.classList.toggle("active", defaultMode === "road"); for (let i = 0; i < legMode.length; i++) { legMode[i] = defaultMode; legGeom[i] = null; if (defaultMode === "road") markDirty(i); } - drawLegs(); updateStats(); if (defaultMode === "road") scheduleRoute(); + redraw(); updateStats(); if (defaultMode === "road") scheduleRoute(); }; document.querySelector("#re-save").onclick = async () => { if (points.length < 2) { showToast("Нужно минимум 2 точки"); return; } let btn = document.querySelector("#re-save"); btn.disabled = true; btn.textContent = "Сохранение..."; - let savePts = fullGeom(); // concat of all legs (road geometry where routed, else straight) - if (savePts.length < 2) { showToast("Маршрут пуст"); btn.disabled = false; btn.textContent = "Сохранить"; return; } + let dense = fullGeom(); // routed geometry with elevations (for display + slope) + if (dense.length < 2) { showToast("Маршрут пуст"); btn.disabled = false; btn.textContent = "Сохранить"; return; } + // Store the editable waypoints + per-leg mode alongside the dense geometry, so re-editing + // shows the 5-6 points again instead of the thousands of routed points. + let wpOut = { waypoints: points.map(p => ({ lat: p.lat, lon: p.lon })), legMode: legMode.slice() }; if (editing) { - await DB.updateRoutePoints(routeId, savePts); - showToast("Маршрут обновлён — загрузите высоты заново"); + await DB.updateRoutePoints(routeId, dense, wpOut); + showToast("Маршрут обновлён"); showRouteDetail(container, routeId); } else { let name = await appPrompt("Название маршрута:", "Мой маршрут", "Сохранить"); if (name == null) { btn.disabled = false; btn.textContent = "Сохранить"; return; } - let id = await DB.addRoute(name.trim() || "Мой маршрут", "#e53935", savePts); + let id = await DB.addRoute(name.trim() || "Мой маршрут", "#e53935", dense, wpOut); showToast("Маршрут создан"); showRouteDetail(container, id); } @@ -4832,7 +4891,9 @@ async function showSessionDetail(sessionId) { L.polyline(ll, { color: "#000", weight: 9, opacity: 0.9 }).addTo(group); // casing L.polyline(ll, { color: "#fff", weight: 6, opacity: 0.9 }).addTo(group); L.polyline(ll, { color: "#00e5ff", weight: 3, opacity: 1 }).addTo(group); // core - } else { + } else if (g.status === "skipped-long" || g.status === "skipped-noroute") { + // a real gap we couldn't bridge — mark it red so the user knows it's unfilled. + // "skipped-nogain" (road ≈ straight) is fine as-is → not drawn, not counted. skippedN++; L.polyline([[g.a.lat, g.a.lon], [g.b.lat, g.b.lon]], { color: "#e53935", weight: 3, opacity: 0.9, dashArray: "6,6" }).addTo(group); } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 22fae57..32a30c1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -139,7 +139,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "baldav" -version = "0.1.450" +version = "0.1.451" dependencies = [ "base64 0.22.1", "btleplug", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2262154..c015dca 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "baldav" -version = "0.1.450" +version = "0.1.451" edition = "2021" [lib] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1a700ee..bff4f58 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json", "productName": "BalDav", - "version": "0.1.450", + "version": "0.1.451", "identifier": "dev.baldav.app", "build": { "frontendDist": "../frontend"