Route editor mid-insert (tap line), track color setting, app dialogs, debug-only orient overlay

- Route editor: tap the route line to insert a point at that spot (nearest
  segment, splice); addMarker -> makeMarker so end-append and mid-insert share
  the drag/delete handlers. Add/drag/delete unchanged.
- Настройка 'Цвет текущего трека' (GPS.config.trackColor, default #bb86fc):
  recording line, detail track + gap dashes, manual-draw line/markers use it —
  so the track is distinguishable from the (blue) roads overlay.
- Replace remaining native dialogs with app ones: new appAlert; tile-path prompt
  -> appPrompt; seed/modal/product-in-use alerts -> appAlert. Selects untouched.
- orient-dbg sensor overlay now only renders when window.__BALDAV_DEBUG (debug
  builds); silent in release.
This commit is contained in:
4aiman 2026-08-22 22:08:58 +03:00
commit de10a02746
2 changed files with 92 additions and 25 deletions

View file

@ -23,6 +23,7 @@ GPS.config = {
useFusedLocation: true, // fused-провайдер (Google Play services) — как Яндекс/Google Maps.
// raw GPS_PROVIDER на устройстве приложению не отдаёт фиксы; fused смешивает GPS+ГЛОНАСС+сеть.
autoFinishIdleMin: 15, // авто-завершить тренировку после N мин без движения (забыл «Конец»)
trackColor: '#bb86fc', // цвет линии текущего/записанного трека (по умолчанию фиолетовый темы)
roadColorWalk: '#8B5E3C', // цвет наложения проезженных/пройденных дорог: ходьба
roadColorBike: '#2266aa', // ... вело
roadColorRun: '#e67e22', // ... бег

View file

@ -180,7 +180,7 @@ async function init() {
let result = await window.__TAURI__.core.invoke("import_seed_products");
if (unlisten) unlisten();
} catch (e) { console.error("[SEED]", e); alert("Seed error: " + e); }
} catch (e) { console.error("[SEED]", e); appAlert("Seed error: " + e); }
// Apply safe area insets (Android status bar / nav bar)
try {
@ -1028,7 +1028,7 @@ function showModal(html, onSubmit, onMount) {
if (ok !== false) { closeModal(); if (ok !== "norender") renderScreen(state.screen); }
} catch (e) {
console.error("[showModal] onSubmit error:", e);
alert("Ошибка: " + e);
appAlert("Ошибка: " + e);
}
};
if (onMount) onMount(overlay);
@ -1103,6 +1103,36 @@ function appPrompt(message, defaultValue = "", okText = "OK") {
});
}
// In-app info dialog (replaces native alert()). One OK button. Returns Promise.
function appAlert(message, okText = "OK") {
return new Promise(resolve => {
let ov = document.createElement("div");
ov.className = "modal-overlay confirm-overlay";
ov.innerHTML = `
<div class="confirm-box">
<div class="confirm-msg">${message}</div>
<div class="confirm-btns">
<button class="btn-primary" id="cf-ok">${okText}</button>
</div>
</div>`;
document.body.appendChild(ov);
let settled = false;
let base = winStack.length;
winPush("alert", () => { ov.remove(); if (!settled) { settled = true; resolve(); } });
let finish = () => {
if (settled) return;
settled = true;
let count = winStack.length - base;
for (let i = 0; i < count; i++) winStack.pop().close();
_skipPops = count;
if (count > 0) history.go(-count);
ov.remove();
resolve();
};
ov.querySelector("#cf-ok").onclick = () => finish();
});
}
// Close the currently-open track-detail modal (plus its fullscreen-map child, if any),
// balance the history stack, then reopen it fresh. Mirrors the recalc/refine pattern
// but pops down to the detail modal so it also works while the map is fullscreen —
@ -1650,7 +1680,7 @@ async function renderProducts(container) {
let pid = parseInt(btn.dataset.pid);
let refs = await DB.getProductRefs(pid);
if (refs.food > 0 || refs.recipes > 0) {
alert(`Нельзя удалить: продукт используется в ${refs.food ? refs.food + " записях еды" : ""}${refs.food && refs.recipes ? " и " : ""}${refs.recipes ? refs.recipes + " рецептах" : ""}`);
await appAlert(`Нельзя удалить: продукт используется в ${refs.food ? refs.food + " записях еды" : ""}${refs.food && refs.recipes ? " и " : ""}${refs.recipes ? refs.recipes + " рецептах" : ""}`);
return;
}
await DB.deleteProduct(pid);
@ -3238,7 +3268,7 @@ async function showRouteEditor(container, routeId) {
<div class="rec-stat"><span id="re-pts" class="rec-val">0</span><span class="rec-lbl">Точек</span></div>
</div>
<div class="rec-bottom">
<span class="prod-hint" style="flex:1;align-self:center;line-height:1.2">тап карты — точка · тащи — двигать · тап точки — удалить</span>
<span class="prod-hint" style="flex:1;align-self:center;line-height:1.2">тап карты — точка · тап линии — вставить · тащи — двигать · тап точки — удалить</span>
<button id="re-undo" class="btn-secondary" style="padding:8px 16px">Отменить</button>
<button id="re-save" class="btn-primary" style="padding:8px 16px">Сохранить</button>
<button id="re-cancel" class="btn-secondary" style="padding:8px 12px;color:#cf6679">Отмена</button>
@ -3268,11 +3298,17 @@ async function showRouteEditor(container, routeId) {
function updatePolyline() {
let latlngs = points.map(p => [p.lat, p.lon]);
if (polyline) map.removeLayer(polyline);
if (latlngs.length >= 2) polyline = L.polyline(latlngs, { color, weight: 4 }).addTo(map);
if (latlngs.length >= 2) {
polyline = L.polyline(latlngs, { color, weight: 4, interactive: true }).addTo(map);
// Tap the LINE to insert a point at that spot — a distinct target from
// markers/empty-map, so it doesn't clash with add-at-end / drag / delete.
polyline.on("click", (e) => { L.DomEvent.stop(e); insertAt(e.latlng); });
}
}
// Draggable + deletable point. Markers stay parallel to `points` (same index),
// so drag updates points[idx] and click (delete) splices both.
function addMarker(p) {
// Draggable + deletable point marker. Markers stay parallel to `points` (same
// index) — drag updates points[idx], click (delete) splices both. Returns the
// marker; caller pushes (end) or splices (mid-insert) into `markers`.
function makeMarker(p) {
let m = L.marker([p.lat, p.lon], { draggable: true, icon: dotIcon });
m.on("drag", () => {
let idx = markers.indexOf(m);
@ -3287,11 +3323,31 @@ async function showRouteEditor(container, routeId) {
updatePolyline(); updateStats();
});
m.addTo(map);
markers.push(m);
return m;
}
// Insert a point where the line was tapped: pick the nearest segment [i,i+1]
// (planar lat/lon distance — fine at city scale) and splice at i+1.
function insertAt(latlng) {
if (points.length < 2) return;
let best = -1, bestD = Infinity;
for (let i = 0; i < points.length - 1; i++) {
let a = points[i], b = points[i + 1];
let dx = b.lon - a.lon, dy = b.lat - a.lat;
let t = (dx || dy) ? ((latlng.lng - a.lon) * dx + (latlng.lat - a.lat) * dy) / (dx * dx + dy * dy) : 0;
t = Math.max(0, Math.min(1, t));
let cx = a.lon + t * dx, cy = a.lat + t * dy;
let d = Math.hypot(latlng.lng - cx, latlng.lat - cy);
if (d < bestD) { bestD = d; best = i; }
}
if (best < 0) return;
let np = { lat: latlng.lat, lon: latlng.lng };
points.splice(best + 1, 0, np);
markers.splice(best + 1, 0, makeMarker(np));
updatePolyline(); updateStats();
}
if (points.length) {
points.forEach(p => addMarker(p));
points.forEach(p => markers.push(makeMarker(p)));
updatePolyline(); updateStats();
map.fitBounds(L.latLngBounds(points.map(p => [p.lat, p.lon])).pad(0.1));
} else if (navigator.geolocation) {
@ -3301,7 +3357,7 @@ async function showRouteEditor(container, routeId) {
map.on("click", e => {
let p = { lat: e.latlng.lat, lon: e.latlng.lng };
points.push(p); addMarker(p);
points.push(p); markers.push(makeMarker(p));
updatePolyline(); updateStats();
});
@ -3389,7 +3445,7 @@ function showDrawRouteScreen(container) {
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);
polyline = L.polyline(latlngs, { color: GPS.config.trackColor || "#bb86fc", weight: 4 }).addTo(map);
}
}
@ -3398,7 +3454,7 @@ function showDrawRouteScreen(container) {
let { lat, lng } = e.latlng;
points.push({ lat, lon: lng });
let marker = L.circleMarker([lat, lng], {
radius: 6, color: "#bb86fc", fillColor: "#bb86fc", fillOpacity: 1, weight: 2
radius: 6, color: GPS.config.trackColor || "#bb86fc", fillColor: GPS.config.trackColor || "#bb86fc", fillOpacity: 1, weight: 2
}).addTo(map);
markers.push(marker);
updatePolyline();
@ -3550,7 +3606,7 @@ async function renderRecordingInline(container) {
setTimeout(() => map.invalidateSize(), 200);
let borderLine = L.polyline([], { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map);
let trailLine = L.polyline([], { color: "#bb86fc", weight: 5 }).addTo(map);
let trailLine = L.polyline([], { color: GPS.config.trackColor || "#bb86fc", weight: 5 }).addTo(map);
// Map orientation: a mode button picks the source — magnetic compass or GPS course.
// Manual 90° steps (↑) rotate the whole screen on top. Two on-map arrows always show
@ -3786,14 +3842,18 @@ async function renderRecordingInline(container) {
smoothedHeading = (smoothedHeading + diff * 0.3 + 360) % 360;
}
// DEBUG overlay: raw sensor values on screen (to verify calibration).
let dbg = document.getElementById("orient-dbg");
if (!dbg) {
dbg = document.createElement("div");
dbg.id = "orient-dbg";
dbg.style.cssText = "position:absolute;bottom:4px;left:4px;z-index:1002;background:rgba(0,0,0,0.85);color:#0f0;font:11px monospace;padding:3px 5px;border-radius:4px;pointer-events:none;white-space:nowrap;";
mapEl.parentElement.appendChild(dbg);
// Only in debug builds — window.__BALDAV_DEBUG is set in init() from
// get_app_info().debug (cfg!(debug_assertions)); silent in release.
if (window.__BALDAV_DEBUG) {
let dbg = document.getElementById("orient-dbg");
if (!dbg) {
dbg = document.createElement("div");
dbg.id = "orient-dbg";
dbg.style.cssText = "position:absolute;bottom:4px;left:4px;z-index:1002;background:rgba(0,0,0,0.85);color:#0f0;font:11px monospace;padding:3px 5px;border-radius:4px;pointer-events:none;white-space:nowrap;";
mapEl.parentElement.appendChild(dbg);
}
dbg.textContent = "a=" + Math.round(e.alpha || 0) + " raw=" + Math.round(raw) + " off=" + Math.round(northOffset) + " h=" + Math.round(heading) + " sm=" + Math.round(smoothedHeading);
}
dbg.textContent = "a=" + Math.round(e.alpha || 0) + " raw=" + Math.round(raw) + " off=" + Math.round(northOffset) + " h=" + Math.round(heading) + " sm=" + Math.round(smoothedHeading);
updateArrows(); // keep the orange compass arrow live even in GPS mode
if (!compassFrozen) applyBearing();
}
@ -4534,7 +4594,7 @@ async function showSessionDetail(sessionId) {
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.polyline(seg, { color: GPS.config.trackColor || "#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);
@ -4542,7 +4602,7 @@ async function showSessionDetail(sessionId) {
// Draw dashed lines for accel-confirmed gaps (GPS lost signal but person was moving)
for (let gl of gapLines) {
L.polyline(gl, { color: "#bb86fc", weight: 3, opacity: 0.5, dashArray: "8, 8" }).addTo(map);
L.polyline(gl, { color: GPS.config.trackColor || "#bb86fc", weight: 3, opacity: 0.5, dashArray: "8, 8" }).addTo(map);
}
// Invisible but tappable hit-targets on top of track
@ -5725,6 +5785,10 @@ async function renderSettings(container) {
</div>
<input id="s-walk-min-speed" type="number" step="0.1" value="${(GPS.config.walkMinSpeedMs * 3.6).toFixed(1)}" style="width:60px;text-align:right;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg);padding:4px 6px;">
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-title">Цвет текущего трека</div><div class="settings-item-desc">Линия записи/трека (чтобы не путать с дорогами)</div></div>
<input id="s-track-color" type="color" value="${GPS.config.trackColor || '#bb86fc'}" style="width:40px;height:28px;border:none;background:none;padding:0">
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-title">Цвет дорог: вело</div></div>
<input id="s-road-color-bike" type="color" value="${GPS.config.roadColorBike || '#2266aa'}" style="width:40px;height:28px;border:none;background:none;padding:0">
@ -5910,7 +5974,7 @@ async function renderSettings(container) {
btn.textContent = prev;
if (!filePath) return; // cancelled
} else {
filePath = prompt("Путь к файлу тайлов (.db):");
filePath = await appPrompt("Путь к файлу тайлов (.db):", "");
if (!filePath) return;
}
let origText = btn.textContent;
@ -6003,13 +6067,15 @@ async function renderSettings(container) {
GPS.config.roadColorBike = container.querySelector("#s-road-color-bike").value;
GPS.config.roadColorWalk = container.querySelector("#s-road-color-walk").value;
GPS.config.roadColorRun = container.querySelector("#s-road-color-run").value;
let trackColorEl = container.querySelector("#s-track-color");
if (trackColorEl) GPS.config.trackColor = trackColorEl.value;
let fusedEl = container.querySelector("#s-use-fused");
if (fusedEl) GPS.config.useFusedLocation = fusedEl.checked;
let gpxzEl = container.querySelector("#s-gpxz-key");
if (gpxzEl) GPS.config.gpxzKey = gpxzEl.value.trim();
GPS.saveConfig();
};
let 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", "s-alt-smooth", "s-walk-max-speed", "s-walk-min-speed", "s-road-color-bike", "s-road-color-walk", "s-road-color-run", "s-gpxz-key"];
let 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", "s-alt-smooth", "s-walk-max-speed", "s-walk-min-speed", "s-track-color", "s-road-color-bike", "s-road-color-walk", "s-road-color-run", "s-gpxz-key"];
for (let id of gpsInputs) {
let el = container.querySelector("#" + id);
if (el) { el.onchange = readGpsConfig; el.oninput = readGpsConfig; }