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:
parent
c1f063ded5
commit
de10a02746
2 changed files with 92 additions and 25 deletions
|
|
@ -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', // ... бег
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue