Map: dark-map toggle (invert tiles only), no repacking

A moon button on the recording, track-detail and roads maps inverts ONLY the
tile layer: body.dark-map applies `invert(1) hue-rotate(180deg) brightness(1.5)`
to .leaflet-tile-pane, so the track/route/roads/puck overlays keep their colours.
Works the same on offline (tiles_*.db) and online tiles — no keys, quotas or
tile repacking. The choice persists in localStorage (baldav_dark_map).

Chosen after ruling out dark tile providers (CARTO/Thunderforest/2GIS/Yandex —
keys/quotas/ToS/projection/RF-reachability) and the transport style (inverts
badly in dense city); plain OSM inverted looks good city and countryside.

Verified on device: filter on the tile pane only, overlay pane filter none,
button active and remembered.
This commit is contained in:
4aiman 2026-09-09 13:33:05 +03:00
commit 5c23c4dda4
5 changed files with 90 additions and 0 deletions

View file

@ -96,6 +96,39 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л
---
## Stage 6 — POI-оверлеи на картах BalDav (АНАЛИЗ, вне BRouter, запрошено 2026-09-09)
**Идея:** показывать OSM-объекты (amenity/shop/tourism) на картах BalDav — при трекинге и просмотре:
ATM, банк, скамейка, туалеты, вода, кафе/бар/фастфуд/ресторан/паб, велопарковка/веломастерская,
автобусная остановка, пекарня/супермаркет/кофе, смотровая, укрытие, пикник и т.д. (набор — как в
brouter-web overlays, см. скрины). С фильтром категорий (тумблеры), иконки скачать и рисовать.
**Что проанализировать перед реализацией:**
- Источник данных POI ОФФЛАЙН: Overpass-выгрузка по региону (bbox ~100км вокруг город) в
локальный стор (SQLite/JSON, как seed/tiles), т.к. сеть в поле недоступна. rd5 не годится как
POI-хранилище. Обновление — по проводу с зеркала (как тайлы/сегменты).
- Иконки: взять набор (напр. используемый brouter-web / maki / OSM-carto sprites), лицензия,
положить в assets, рисовать как Leaflet divIcon/marker с кластеризацией на низком зуме (перф).
- UI: панель категорий с чекбоксами (как в brouter-web), сохранение выбора (localStorage),
z-order (POI над тайлами/дорогами, под треком/паком).
- Перф: сотни-тысячи точек — кластеризация/культинг по зуму и bbox видимой области.
**Статус:** not started (анализ).
## Stage 7 — Тёмные тайлы карты в BalDav (запрошено 2026-09-09)
**Идея:** заменить фон OSM на тёмный (пользователю нравится ÖPNV, но хочет тёмное). ВАЖНО (проверено
2026-09-09): хорошего тёмного растра БЕЗ ключа сейчас нет — CARTO Dark теперь тоже требует ключ
(без ключа отдаёт тайлы с ватермаркой "API KEY REQUIRED"). Кандидаты: Thunderforest Transport Dark
(ÖPNV-стиль тёмный, ближе всего к желаемому — бесплатный ключ), CARTO Dark (ключ), Stadia (ключ);
без ключа и чисто — только светлые (ÖPNVKarte/OpenTopoMap/OSM) или спутник Esri. Источник в
`frontend/js/tilecache.js` хардкодом
`https://a.tile.openstreetmap.org/{z}/{x}/{y}.png` (3 места); оффлайн-тайлы — в `tiles_*.db` (OSM).
**РЕШЕНО (2026-09-09): живой CSS-фильтр, без перепаковки.** Провайдеров тёмных тайлов (CARTO/
Thunderforest/2GIS/Yandex) отмели — ключи/квоты/ToS/проекция/недоступность в РФ. Транспортная карта
инвертом плохо (город). ПОБЕДИЛО: OSM-стандарт (те тайлы, что уже есть) + фильтр
`invert(1) hue-rotate(180deg) brightness(1.5)` на `.leaflet-tile-pane` — достойно и в городе, и на селе.
Реализация: класс `body.dark-map` + одно CSS-правило (только слой тайлов, оверлеи цветные), тумблер
`☾` на экранах записи/деталей трека/дорог (`toggleDarkMap`/`applyDarkMap`/`addDarkMapControl` в main.js),
выбор в localStorage `baldav_dark_map`. Работает одинаково на оффлайн-БД и онлайне, без ключей/квот.
**Статус:** DONE (проверено на устройстве: фильтр на тайлах, overlay `none`, кнопка активна, персист).
## Потенциальные проблемы
- Desugaring/`java.lang.invoke` при dex — проверить сборку (AGP обычно тянет).
- Размер/имена тайлов региона на brouter.de — уточнить при Stage 2.

View file

@ -19,6 +19,7 @@
- **Высоты маршрута (рельеф до поездки)** — экран предпросмотра маршрута (тап по названию): профиль высоты (крутые участки красным), длина/набор/сброс/макс. уклон, тап по точке — высота и уклон, разворот карты на весь экран. Топографическая обводка линии с тогглом **уклон/высота**: уклон дискретными полосами по 1% (зелёный→жёлтый→красный→коричневый→чёрный, ≥10% чёрный) или гипсометрия по высоте (зелёный→жёлтый→коричневый). Обводка и тоггл доступны и в оверлее на карте записанного трека и во время записи; маршрут рисуется ПОД треком. Высоты (`<ele>` из GPX или онлайн) тянутся по цепочке: **GPXZ** (Copernicus GLO-30, точнее для уклонов; ключ в GPS-настройках, хранится только на устройстве) → open-elevation → open-meteo → OpenTopoData (без ключа). Калории пересчитываются по сглаженным точкам; для уклона высоты сглаживаются (гасит вертикальный шум SRTM).
- **Редактор маршрутов** — создание («Новый маршрут») и правка сохранённого маршрута прямо в приложении: тап по карте — добавить точку в конец, тап по линии — вставить точку в это место, тащить точку — двигать, тап по точке — удалить. Высоты подтягиваются онлайн после отрисовки (из предпросмотра).
- **Де-джиттер трека** — сглаживание дрожащего GPS-трека методом Савицкого-Голея (настраиваемое окно/число итераций, живое превью на карте детали трека). Хранится обратимо: оригинал и сглаженный вариант лежат раздельно, калории пересчитываются по сглаженным точкам, откат к оригиналу в один тап. Точность каждого фикса (`accuracy_m`) пишется в БД: при пересчёте трека точки хуже порога «Макс. погрешность GPS» исключаются из расчёта и линии (обратимо — точки в БД остаются; поднять порог = вернуть). Работает во всех пересчётах (кнопка «Пересчитать», «Пересчитать всё», после де-джиттера); импорт/старьё без точности и dead-reckoning заполнения не трогает.
- **Тёмная карта** — тумблер `☾` на экранах записи, деталей трека и дорог: инвертирует ТОЛЬКО слой тайлов (`invert + hue-rotate 180° + brightness`, трек/маршрут/дороги остаются цветными), выбор запоминается. Работает одинаково на офлайн-тайлах и онлайне, без ключей/квот/перепаковки.
- **Метки ветра** — кнопка «Ветер в лицо» на экране записи: тап открывает интервал встречного ветра, тап закрывает. Метки хранятся с треком (`wind_marks`) как эталон для проверки оценки ветра по треку; на расчёт калорий/мощности не влияют. Голосовые метки — на будущее.
- **Экспорт/импорт** — свои продукты, рецепты и добавки одним файлом (JSON).
- **Вес, сон, добавки** — логирование с историей.

View file

@ -74,3 +74,16 @@ readTimeout 60с, до 30 попыток. Итог на устройстве: E4
LAN-зеркала за <2с, маршрут на нём ок.
Осталось: WAN-пробросы на Keenetic (PPPoE0 :8088→роутер:88, :17777→ПК:17777) — outward-facing,
с подтверждением; для release добавить network-security-config под cleartext на mirror-хосты.
## Дополнение 2026-09-09 (4): своё зеркало на роутере + BRouter-Web + тёмная карта
- Зеркало на Keenetic: тайлы+профили в /opt/share/www/brouter/ (uhttpd :88), доступно LAN 192.168.1.1:88
и WAN mirror.invalid:8088 (static-NAT PPPoE0 :8088→:88). BalDav качает тайлы с зеркала (LAN→WAN→brouter.de).
- BRouter RouteServer на ПК: /mnt/02F6E7F6F6E7E7BD/brouter-mirror, СИСТЕМНЫЙ systemd brouter.service
(multi-user.target, автостарт без логина, поставил пользователь sudo). WAN :17777→ПК:17777.
- BRouter-Web (nrenner 0.18.1) на зеркале: http://192.168.1.1:88/brouter-web/ (+WAN), config.js → RouteServer,
дефолт-слой ÖPNV (CARTO/др. — «needs key»), центр город.
- Тёмная карта в BalDav: перебрали провайдеров (CARTO/Thunderforest/2GIS/Yandex — ключи/квоты/ToS/проекция/
недоступность в РФ; проба доступности из BalDav по мобильному Tele2: доступны OSM/CARTO/ÖPNV/Esri/2GIS,
нет OpenTopo/Yandex-vec). Транспортную инвертом — плохо в городе. Решение: OSM-стандарт + CSS
`invert(1) hue-rotate(180deg) brightness(1.5)` на .leaflet-tile-pane, тумблер ☾ (запись/детали/дороги),
без перепаковки. Проверено на устройстве.

View file

@ -1145,6 +1145,10 @@ select {
.rec-roads-toggle { right: 8px; bottom: 124px; }
.rec-mode-btn { right: 8px; bottom: 162px; font-size: 11px; }
.rec-route-toggle { right: 8px; bottom: 200px; }
.rec-dark-toggle { right: 8px; bottom: 238px; }
/* Dark map: invert ONLY the tile layer (overlays — track, route, roads, puck — keep
their colours). User-tuned filter on OSM standard tiles. Applies to every Leaflet map. */
body.dark-map .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(1.5); }
/* Always-on compass needle: red half = North, grey = South. Rotates to point to real north.
Top-RIGHT (zoom controls are top-left), below the status-bar safe area. */
/* Position/direction puck (SVG divIcon) — no default leaflet-div-icon box. */

View file

@ -114,6 +114,7 @@ function winPopTop() {
// ---------------------------------------------------------------------------
async function init() {
applyDarkMap(); // restore dark-map tile filter preference before any map renders
if (typeof DB === "undefined" || typeof DB.getFullProfile !== "function") {
app.innerHTML = `<div class="loading" style="color:#cf6679">
Ошибка: db.js не загрузился.<br>
@ -4102,6 +4103,14 @@ async function renderRecordingInline(container) {
}
routeBtn.onclick = () => setRouteShown(!routeLayer);
btnWrap.appendChild(routeBtn);
// Dark-map toggle (moon): invert the tile layer only, remembered globally.
let darkBtn = document.createElement("button");
darkBtn.className = "rec-map-btn rec-dark-toggle" + (isDarkMap() ? " active" : "");
darkBtn.textContent = "☾";
darkBtn.title = "Тёмная карта";
darkBtn.onclick = () => toggleDarkMap();
btnWrap.appendChild(darkBtn);
// 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.
@ -4723,6 +4732,7 @@ async function showSessionDetail(sessionId) {
addZoomDisplay(map);
let CachedLayer = TileCache.createLayer();
new CachedLayer({ maxZoom: 19 }).addTo(map);
addDarkMapControl(map); // dark-tiles toggle on the track view
// Active saved-route overlay on the detail map + a toggle button (default ON when
// a route is selected). Shares baldav_route_shown with the recording screen.
@ -7348,6 +7358,7 @@ async function showRoadsMap() {
addZoomDisplay(map);
let CachedLayer = TileCache.createLayer();
new CachedLayer({ maxZoom: 19 }).addTo(map);
addDarkMapControl(map); // dark-tiles toggle on the roads map
let infoEl = document.querySelector("#roads-info");
infoEl.textContent = "Загрузка...";
@ -7597,6 +7608,34 @@ function copyToClipboard(text, toastMsg) {
} else fallback();
}
// Dark map: invert the tile layer only (overlays keep their colours). User-tuned filter
// on OSM standard tiles — invert(1) hue-rotate(180deg) brightness(1.5). Toggled by the
// user, remembered in localStorage, applied globally to every Leaflet tile pane. No
// tile repacking: works the same on offline (tiles_*.db) and online tiles.
function isDarkMap() { try { return localStorage.getItem("baldav_dark_map") === "1"; } catch (e) { return false; } }
function applyDarkMap() { document.body.classList.toggle("dark-map", isDarkMap()); }
function toggleDarkMap() {
let on = !isDarkMap();
try { localStorage.setItem("baldav_dark_map", on ? "1" : "0"); } catch (e) {}
document.body.classList.toggle("dark-map", on);
document.querySelectorAll(".map-dark-btn, .rec-dark-toggle").forEach(b => b.classList.toggle("active", on));
return on;
}
// Leaflet control button (top-right) for maps without a custom button stack.
function addDarkMapControl(map) {
let Ctl = L.Control.extend({
options: { position: "topright" },
onAdd: function () {
let b = L.DomUtil.create("button", "rec-map-btn map-dark-btn" + (isDarkMap() ? " active" : ""));
b.textContent = "☾"; b.title = "Тёмная карта"; b.style.position = "static";
L.DomEvent.disableClickPropagation(b);
b.onclick = () => toggleDarkMap();
return b;
},
});
map.addControl(new Ctl());
}
function esc(str) {
return String(str ?? "")
.replace(/&/g, "&amp;").replace(/</g, "&lt;")