Reroute: rejoin the active route past a blockage from the current position
Stage 2. Recording-map button ↪ builds a detour from the current GPS position back ONTO the active route past the blockage: reference = nearest nogo on the route (else current pos), target = a route point ~1.5km further (steps further on failure to the route end), avoiding nogos and 180° turns (bearing helper). Drawn as a transient magenta navigation overlay with the rejoin point; long-press the button clears it. Does not modify the saved route or interrupt recording.
This commit is contained in:
parent
1c326ab19c
commit
e40f6e25b7
3 changed files with 105 additions and 1 deletions
|
|
@ -21,6 +21,7 @@
|
|||
- **Оффлайн-роутинг (BRouter)** — движок BRouter встроен в приложение (Android): маршруты строятся по дорогам без сети, сегменты `.rd5` региона качаются с домашнего зеркала прямо в приложении (экран дорог → «Сегменты»). Используется в редакторе маршрутов («По дорогам») и для заполнения GPS-пропусков. На десктопе/дома движка нет — те же вызовы уходят на домашний BRouter-RouteServer по HTTP.
|
||||
- **Заполнение GPS-пропусков по дороге** — на детали трека кнопка **«🧩»** прокладывает дорожный путь (BRouter, профиль shortest) через каждый GPS-дропаут (разрыв по времени + реальная хорда) и показывает **предпросмотр только достроенных пропусков** (голубым, с бело-чёрной оковкой; недостроенные — красным пунктиром) плюс панель «Достроено N (+X км), M не удалось» с кнопками **Сохранить/Отмена**. Сохранение — **обратимый патч на месте**: достроенные точки помечаются `estimated` и добавляются в трек (оригинал цел), статистика пересчитывается; повторный тап предлагает **«Убрать достройку»** (удаляет только достроенное). Достраиваются только реальные длинные потери сигнала (по умолчанию разрыв > 60с и хорда > 150м); пропуск не достраивается, если дорога заметно длиннее хорды (крюк, > 2×) или почти совпадает с прямой (мостить нечего) — чтобы не перерисовывать нормальные участки объездами.
|
||||
- **Непроезжие места (nogo)** — красные круги, которые роутинг ОБЪЕЗЖАЕТ. Ставятся кнопкой **⛔**: в планировщике — тапом по карте/линии, на карте записи — «здесь нет проезда» (текущее место). Показываются на картах планировщика, записи и детали трека; тап по кругу — убрать. Избегание работает и через домашний RouteServer, и встроенным движком офлайн (BRouter nogopoints). Web-BRouter умеет nogo сам.
|
||||
- **Объезд с возвратом на маршрут (на ходу)** — на карте записи кнопка **«↪»**: если активный маршрут ведёт через непроезжее место, строит от текущей позиции объезд обратно НА маршрут за местом затыка (отступ ~1.5 км вперёд, растёт при неудаче до конца маршрута), избегая nogo и разворота на 180° (учитывает курс). Показывается ярким навигационным оверлеем (не меняет сохранённый маршрут и не прерывает запись); долгий тап по кнопке убирает оверлей.
|
||||
- **Де-джиттер трека** — сглаживание дрожащего GPS-трека методом Савицкого-Голея (настраиваемое окно/число итераций, живое превью на карте детали трека). Хранится обратимо: оригинал и сглаженный вариант лежат раздельно, калории пересчитываются по сглаженным точкам, откат к оригиналу в один тап. Точность каждого фикса (`accuracy_m`) пишется в БД: при пересчёте трека точки хуже порога «Макс. погрешность GPS» исключаются из расчёта и линии (обратимо — точки в БД остаются; поднять порог = вернуть). Работает во всех пересчётах (кнопка «Пересчитать», «Пересчитать всё», после де-джиттера); импорт/старьё без точности и dead-reckoning заполнения не трогает.
|
||||
- **POI на карте** — точки интереса (кафе, магазины, вода, туалеты, банки/ATM, остановки, укрытия, смотровые и т.д.) круглыми контрастными значками (эмодзи в кружке, читаемо на светлой и тёмной карте). Кнопка `📍` с панелью-фильтром категорий; рендер по видимой области от зума 13. Данные оффлайн (`data/pois.json`, ~100 км вокруг опорной точки, 5662 объекта из OSM/Overpass). На картах записи, деталей трека и дорог.
|
||||
- **Тёмная карта** — тумблер `☾` на экранах записи, деталей трека и дорог: инвертирует ТОЛЬКО слой тайлов (`invert + hue-rotate 180° + brightness`, трек/маршрут/дороги остаются цветными), выбор запоминается. Работает одинаково на офлайн-тайлах и онлайне, без ключей/квот/перепаковки.
|
||||
|
|
|
|||
|
|
@ -29,5 +29,22 @@
|
|||
- ДЕСКТОП-ТЕСТ планировщика (nogo обходится визуально) — следующий шаг.
|
||||
- Встроенный движок (офлайн) — после `./build.sh android -d -i`, на теле.
|
||||
|
||||
## Проверка Стадии 1 (десктоп Xvfb + RouteServer)
|
||||
- Планировщик: 2 точки → дорожная нога; ⛔ → тап по линии ставит красный круг → нога
|
||||
перестроилась в обход (nogo в БД: 47.4238,40.087,25м). Тестовые nogo/маршруты убраны.
|
||||
- RouteServer `&nogos`: 2873м → 3329м. Коммит `1c326ab`.
|
||||
|
||||
## Сделано — Стадия 2 (объезд с возвратом на маршрут)
|
||||
- `rejoinReroute(cur, bearing, routePts, nogos)` (main.js): опорная точка затыка = ближайший
|
||||
nogo на маршруте (≤150м), иначе текущая позиция; цель = точка маршрута на +1.5 км вперёд,
|
||||
шаг +1.5 км до конца при неудаче; помощник по курсу (~40 м) чтобы не разворачиваться; принять,
|
||||
если длина ≤ max(500, 3× прямой). Не меняет сохранённый маршрут.
|
||||
- Карта записи: кнопка «↪» — строит объезд от `lastMovingPoint` к точке возврата, рисует ярким
|
||||
оверлеем (magenta с бело-чёрной оковкой) + метка возврата; долгий тап убирает.
|
||||
|
||||
## Проверка Стадии 2
|
||||
- Синтаксис ок. Алгоритм `rejoinReroute` проверить харнессом против RouteServer (десктоп).
|
||||
- Полный прогон на ходу + встроенный движок офлайн — на теле после `./build.sh android -d -i`.
|
||||
|
||||
## Дальше
|
||||
- Стадия 2: объезд с возвратом на маршрут на карте записи (`rejoinReroute` + кнопка «объехать»).
|
||||
- Сборка/установка на тело — по явной просьбе. Реальная поездка: пометить непроезжее, «↪».
|
||||
|
|
|
|||
|
|
@ -3196,6 +3196,56 @@ async function getNogosForRouter() {
|
|||
return (await DB.getNogoAreas()).map(a => ({ lat: a.lat, lon: a.lon, radius: a.radius_m || 25 }));
|
||||
}
|
||||
|
||||
// Move a point `d` metres along bearing `brDeg` (for a "start forward, don't U-turn" helper).
|
||||
function _offsetLatLon(lat, lon, brDeg, d) {
|
||||
let R = 6378137, br = brDeg * Math.PI / 180, la1 = lat * Math.PI / 180, lo1 = lon * Math.PI / 180;
|
||||
let la2 = Math.asin(Math.sin(la1) * Math.cos(d / R) + Math.cos(la1) * Math.sin(d / R) * Math.cos(br));
|
||||
let lo2 = lo1 + Math.atan2(Math.sin(br) * Math.sin(d / R) * Math.cos(la1), Math.cos(d / R) - Math.sin(la1) * Math.sin(la2));
|
||||
return { lat: la2 * 180 / Math.PI, lon: lo2 * 180 / Math.PI };
|
||||
}
|
||||
|
||||
// On-the-fly detour: from the current position, build a connector back ONTO the saved route
|
||||
// PAST the blockage, avoiding nogos, preferring to start in the travel direction (no 180 if a
|
||||
// bearing is given). Does NOT modify the saved route. Blockage reference = nearest nogo that
|
||||
// sits on the route (<=150 m), else the current position's nearest route point. Target = a
|
||||
// route point ~1.5 km further along; on failure step further (up to the route end).
|
||||
// Returns { points:[{lat,lon}], target:{lat,lon} } or null.
|
||||
async function rejoinReroute(cur, bearingDeg, routePts, nogos) {
|
||||
if (!cur || !routePts || routePts.length < 2) return null;
|
||||
let cum = [0];
|
||||
for (let i = 1; i < routePts.length; i++) cum[i] = cum[i-1] + GPS.distance(routePts[i-1].lat, routePts[i-1].lon, routePts[i].lat, routePts[i].lon);
|
||||
let nearestRouteDist = (pt) => { let d = Infinity; for (let p of routePts) { let dd = GPS.distance(pt.lat, pt.lon, p.lat, p.lon); if (dd < d) d = dd; } return d; };
|
||||
// reference point of the blockage
|
||||
let ref = cur;
|
||||
if (nogos && nogos.length) {
|
||||
let best = null, bestD = Infinity;
|
||||
for (let n of nogos) if (nearestRouteDist(n) <= 150) { let dc = GPS.distance(cur.lat, cur.lon, n.lat, n.lon); if (dc < bestD) { bestD = dc; best = n; } }
|
||||
if (best) ref = best;
|
||||
}
|
||||
let refIdx = 0, refD = Infinity;
|
||||
for (let i = 0; i < routePts.length; i++) { let d = GPS.distance(ref.lat, ref.lon, routePts[i].lat, routePts[i].lon); if (d < refD) { refD = d; refIdx = i; } }
|
||||
let helper = (bearingDeg != null && !isNaN(bearingDeg)) ? _offsetLatLon(cur.lat, cur.lon, bearingDeg, 40) : null;
|
||||
let skip = 1500;
|
||||
for (let attempt = 0; attempt < 8; attempt++) {
|
||||
let want = cum[refIdx] + skip, ti = refIdx;
|
||||
while (ti < routePts.length - 1 && cum[ti] < want) ti++;
|
||||
let target = routePts[ti];
|
||||
let straight = GPS.distance(cur.lat, cur.lon, target.lat, target.lon);
|
||||
let wps = helper ? [[cur.lon, cur.lat], [helper.lon, helper.lat], [target.lon, target.lat]] : [[cur.lon, cur.lat], [target.lon, target.lat]];
|
||||
try {
|
||||
let gpx = await BRouter.route(wps, "trekking", nogos);
|
||||
let rp = (GPS.parseRouteGPX(gpx) || {}).points || [];
|
||||
if (rp.length >= 2) {
|
||||
let rlen = 0; for (let k = 1; k < rp.length; k++) rlen += GPS.distance(rp[k-1].lat, rp[k-1].lon, rp[k].lat, rp[k].lon);
|
||||
if (rlen <= Math.max(500, straight * 3)) return { points: rp.map(p => ({ lat: p.lat, lon: p.lon })), target: { lat: target.lat, lon: target.lon } };
|
||||
}
|
||||
} catch (e) { /* try a further rejoin point */ }
|
||||
if (ti >= routePts.length - 1) break;
|
||||
skip += 1500;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Slope tint by absolute grade in %, in 1%-wide discrete bands (hard boundaries
|
||||
// kept — one colour per segment): green -> yellow -> red -> brown -> black.
|
||||
// <1%=0 .. 9–10%=9, >=10% = black. Both up- and downhill coloured by magnitude.
|
||||
|
|
@ -4323,6 +4373,42 @@ async function renderRecordingInline(container) {
|
|||
};
|
||||
btnWrap.appendChild(recNogoBtn);
|
||||
|
||||
// Reroute back onto the active route past a blockage (nogo), from the current position,
|
||||
// avoiding nogos and 180° turns. Transient navigation overlay — does NOT touch the saved
|
||||
// route or the recording. Tap again to rebuild from the new position; long-press to clear.
|
||||
let rerouteLayer = null;
|
||||
let rerouteBtn = document.createElement("button");
|
||||
rerouteBtn.className = "rec-map-btn";
|
||||
rerouteBtn.textContent = "↪";
|
||||
rerouteBtn.title = "Объехать: вернуться на маршрут за непроезжим местом";
|
||||
rerouteBtn.onclick = async () => {
|
||||
let rid = localStorage.getItem("baldav_active_route");
|
||||
if (!rid) { showToast("Нет активного маршрута"); return; }
|
||||
let cur = GPS.recording.lastMovingPoint;
|
||||
if (!cur) { showToast("Нет позиции"); return; }
|
||||
rerouteBtn.disabled = true; rerouteBtn.classList.add("active");
|
||||
try {
|
||||
let route = await DB.getRoute(rid);
|
||||
if (!route || !route.points || route.points.length < 2) { showToast("Маршрут пуст"); return; }
|
||||
let ng = await getNogosForRouter();
|
||||
let bearing = (typeof smoothedHeading === "number") ? smoothedHeading : null;
|
||||
let res = await rejoinReroute(cur, bearing, route.points, ng);
|
||||
if (!res) { showToast("Объезд не найден"); rerouteBtn.classList.remove("active"); return; }
|
||||
if (rerouteLayer) { map.removeLayer(rerouteLayer); rerouteLayer = null; }
|
||||
let ll = res.points.map(p => [p.lat, p.lon]);
|
||||
let g = L.layerGroup();
|
||||
L.polyline(ll, { color: "#000", weight: 9, opacity: 0.9 }).addTo(g);
|
||||
L.polyline(ll, { color: "#fff", weight: 6, opacity: 0.9 }).addTo(g);
|
||||
L.polyline(ll, { color: "#ff00ff", weight: 3, opacity: 1 }).addTo(g);
|
||||
L.circleMarker([res.target.lat, res.target.lon], { radius: 7, color: "#ff00ff", fillColor: "#fff", fillOpacity: 1, weight: 3 }).addTo(g);
|
||||
g.addTo(map); rerouteLayer = g;
|
||||
showToast("Объезд построен");
|
||||
} catch (e) { showToast("Объезд: " + (e.message || e)); }
|
||||
finally { rerouteBtn.disabled = false; }
|
||||
};
|
||||
rerouteBtn.oncontextmenu = (e) => { e.preventDefault(); if (rerouteLayer) { map.removeLayer(rerouteLayer); rerouteLayer = null; rerouteBtn.classList.remove("active"); showToast("Объезд убран"); } };
|
||||
btnWrap.appendChild(rerouteBtn);
|
||||
|
||||
POI.attach(map); // POI overlay + category filter on the recording map
|
||||
// The default-ON draw is deferred to the END of this function (after all the
|
||||
// critical wiring), where it awaits whenMapReady — so it can't delay setup.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue