@@ -1188,6 +1187,8 @@ function generateGPX() {
if (!route) return '';
const now = new Date();
const dateStr = now.toISOString().slice(0, 10);
+ // ET-005: экспорт GPX остаётся метрическим — стандарт GPX и риск R6
+ // исключают конвертацию выгружаемых данных в мили.
const distKm = (route.distance_m / 1000).toFixed(1);
const dirtPct = route.stats ? route.stats.dirt_total_pct : '?';
@@ -1412,9 +1413,22 @@ async function initMap() {
const targetPx = 80;
const rawMeters = metersPerPixel * targetPx;
-
+
+ // ET-005 (риск R3): масштабная линейка учитывает выбранную единицу.
+ // niceMeters всегда остаётся в метрах — по нему считается ширина в px.
let distance, unit, niceMeters;
- if (rawMeters >= 1000) {
+ if (window.Units && Units.getUnit() === 'mi') {
+ const rawMiles = (rawMeters / 1000) * Units.KM_TO_MI;
+ if (rawMiles >= 1) {
+ distance = rawMiles >= 100 ? Math.round(rawMiles / 50) * 50 :
+ rawMiles >= 10 ? Math.round(rawMiles / 5) * 5 :
+ Math.round(rawMiles);
+ } else {
+ distance = Math.max(0.1, Math.round(rawMiles * 10) / 10);
+ }
+ unit = 'mi';
+ niceMeters = (distance / Units.KM_TO_MI) * 1000;
+ } else if (rawMeters >= 1000) {
const km = rawMeters / 1000;
distance = km >= 100 ? Math.round(km / 50) * 50 :
km >= 10 ? Math.round(km / 5) * 5 :
@@ -1441,6 +1455,8 @@ async function initMap() {
zoomEl.textContent = 'z' + zoom;
}
+ // ET-005: оркестратор onUnitChange() обновляет линейку при смене единицы.
+ window._updateScaleZoom = updateScaleZoom;
updateScaleZoom();
map.on('zoom', updateScaleZoom);
map.on('move', updateScaleZoom);
@@ -1473,10 +1489,10 @@ async function initMap() {
maxWidth: '300px',
});
+ // ET-005: расстояние во всплывающих подсказках — через units.js (ADR-0001).
function formatLength(m) {
if (!m) return '-';
- if (m >= 1000) return (m / 1000).toFixed(1) + ' км';
- return Math.round(m) + ' м';
+ return Units.formatDistance(m);
}
function poiTypeLabel(t) {
@@ -1834,7 +1850,8 @@ function updateRulerLine() {
});
}
// Update ruler info display
- const dist = rulerTotal >= 1 ? rulerTotal.toFixed(1) + ' км' : Math.round(rulerTotal * 1000) + ' м';
+ // ET-005: rulerTotal хранится в км — переводим в метры для units.js.
+ const dist = Units.formatDistance(rulerTotal * 1000);
document.getElementById('ruler-dist').textContent = dist;
}
@@ -1871,9 +1888,8 @@ function updateRulerLabels() {
const segDist = haversineKm(rulerPoints[i - 1], rulerPoints[i]);
rulerTotal += segDist;
if (labelText) {
- labelText.textContent = segDist >= 1
- ? segDist.toFixed(1) + ' км'
- : Math.round(segDist * 1000) + ' м';
+ // ET-005: segDist в км — переводим в метры для units.js.
+ labelText.textContent = Units.formatDistance(segDist * 1000);
}
}
// Update remove button index
@@ -1882,7 +1898,8 @@ function updateRulerLabels() {
}
}
// Update total display
- const dist = rulerTotal >= 1 ? rulerTotal.toFixed(1) + ' км' : Math.round(rulerTotal * 1000) + ' м';
+ // ET-005: rulerTotal хранится в км — переводим в метры для units.js.
+ const dist = Units.formatDistance(rulerTotal * 1000);
document.getElementById('ruler-dist').textContent = dist;
}
@@ -1927,9 +1944,8 @@ function addRulerPoint(lngLat) {
if (idx === 0) {
labelText.textContent = 'Старт';
} else {
- labelText.textContent = segDist >= 1
- ? segDist.toFixed(1) + ' км'
- : Math.round(segDist * 1000) + ' м';
+ // ET-005: segDist в км — переводим в метры для units.js.
+ labelText.textContent = Units.formatDistance(segDist * 1000);
}
// Bug 5: use button element for better tap target and semantics
@@ -2105,6 +2121,9 @@ function clearRecon() {
let linkMode = false;
let linkPoints = [];
let linkMarkers = [];
+// ET-005: последние построенные связки кэшируются, чтобы оркестратор
+// onUnitChange() мог перерисовать карточки без повторного запроса к API.
+let linkRoutes = [];
function toggleLinkMode() {
const btn = document.getElementById('tb-link');
@@ -2178,6 +2197,7 @@ async function buildLinkRoute() {
function renderLinkCards(routes) {
const map = window._map;
+ linkRoutes = routes; // ET-005: кэш для перерисовки при смене единицы
const colors = ['#0066ff', '#00aa44', '#9933cc'];
const cardsEl = document.getElementById('link-cards');
cardsEl.innerHTML = '';
@@ -2199,7 +2219,6 @@ function renderLinkCards(routes) {
layout: { 'line-cap': 'round', 'line-join': 'round' }
});
- const km = (r.distance_m / 1000).toFixed(0);
const time = formatDuration(r.duration_s);
const dirt = r.stats?.dirt_total_pct || '?';
const col = colors[i % colors.length];
@@ -2209,7 +2228,7 @@ function renderLinkCards(routes) {
${dirt}% грунт
@@ -2354,7 +2373,6 @@ function drawScenicRoutes(routes, activeIdx) {
if (cardsEl) {
cardsEl.innerHTML = routes.map((r, i) => {
const col = colors[i % colors.length];
- const km = (r.distance_m / 1000).toFixed(0);
const time = formatDuration(r.duration_s);
const dirt = r.stats?.dirt_total_pct || '?';
const pois = (r.scenic_pois || []).map(p => {
@@ -2367,7 +2385,7 @@ function drawScenicRoutes(routes, activeIdx) {
${dirt}% грунт · score=${r.scenic_score||0}
@@ -2414,6 +2432,10 @@ document.addEventListener('DOMContentLoaded', () => {
initSheetSwipe();
// Apply saved theme immediately (before map loads)
applyTheme();
+ // ET-005: восстановить выбор единиц измерения (AC-3) и подключить
+ // единый оркестратор пересчёта расстояний (ADR-0001 п.6).
+ syncUnitToggleUI();
+ document.addEventListener('unitchange', onUnitChange);
});
// ─── Mini Route Bar ──────────────────────────────────────────────────
@@ -2602,11 +2624,10 @@ function hideMiniRouteSheet() {
function updateMiniRouteCard() {
const r = routeResults[activeRouteIdx];
if (!r) return;
- const km = (r.distance_m / 1000).toFixed(1);
const dirt = r.stats?.dirt_total_pct ?? '-';
document.getElementById('mini-dot').style.background = ROUTE_COLORS[activeRouteIdx % ROUTE_COLORS.length];
document.getElementById('mini-label').textContent = `Вариант ${activeRouteIdx + 1} из ${routeResults.length}`;
- document.getElementById('mini-stats').textContent = `${km} км · ${dirt}% грунт`;
+ document.getElementById('mini-stats').textContent = `${Units.formatDistance(r.distance_m)} · ${dirt}% грунт`;
document.getElementById('mini-prev').style.opacity = activeRouteIdx > 0 ? '1' : '0.3';
document.getElementById('mini-next').style.opacity = activeRouteIdx < routeResults.length - 1 ? '1' : '0.3';
}
@@ -2853,6 +2874,79 @@ function restorePoiState() {
}
// <<< ET-002 POI visibility block <<<
+// >>> ET-005 unit toggle block >>>
+// Переключатель единиц измерения расстояний (км/мили) в попапе рельефа.
+// Выбор единицы, его персистентность и форматирование вынесены в
+// src/web/units.js (ADR-0001). Здесь — UI-обработчик попапа и единый
+// оркестратор пересчёта видимых расстояний.
+// См. docs/work-items/ET-005/06-adr/adr-0001-unit-toggle-client-side.md
+
+/**
+ * Обработчик сегментированного переключателя единиц в попапе рельефа
+ * (атрибут onclick кнопок «км» / «мили»).
+ *
+ * Делегирует смену единицы модулю Units. Пересчёт всех видимых
+ * расстояний выполняет оркестратор onUnitChange() по событию
+ * 'unitchange'; здесь дополнительно синхронизируется вид кнопок.
+ * @param {('km'|'mi')} unit - выбранная единица измерения.
+ */
+function onUnitToggle(unit) {
+ Units.setUnit(unit);
+ syncUnitToggleUI();
+}
+
+/**
+ * Синхронизирует визуальное состояние кнопок «км» / «мили» с текущей
+ * выбранной единицей измерения.
+ *
+ * Вызывается при инициализации страницы (восстановление выбора из
+ * localStorage — AC-3) и после каждого переключения.
+ */
+function syncUnitToggleUI() {
+ const unit = Units.getUnit();
+ const kmBtn = document.getElementById('unit-btn-km');
+ const miBtn = document.getElementById('unit-btn-mi');
+ if (kmBtn) kmBtn.classList.toggle('active', unit === 'km');
+ if (miBtn) miBtn.classList.toggle('active', unit === 'mi');
+}
+
+/**
+ * Единый оркестратор пересчёта расстояний при смене единицы измерения
+ * (ADR-0001 п.6). Подписан на событие 'unitchange' ровно один раз —
+ * вместо россыпи подписок по компонентам пере-вызывает функции
+ * отрисовки всех видимых поверхностей с расстояниями.
+ *
+ * Внутреннее состояние остаётся метрическим: конвертация выполняется
+ * исключительно в Units.formatDistance().
+ */
+function onUnitChange() {
+ // Карточки основного маршрута, лист точек и мини-карточка.
+ if (routeResults.length > 0) {
+ renderRouteCards(routeResults);
+ updateMiniRouteCard();
+ }
+ if (routeWaypoints.length > 0) {
+ renderWaypointsList();
+ }
+ // Линейка: updateRulerLabels() обновляет и подписи отрезков, и итог.
+ if (rulerMarkers.length > 0) {
+ updateRulerLabels();
+ }
+ // Карточки связки и «красивого» маршрута — только при активном режиме,
+ // чтобы перерисовка не возвращала на карту скрытые слои.
+ if (linkMode && linkRoutes.length > 0) {
+ renderLinkCards(linkRoutes);
+ }
+ if (scenicMode && scenicRoutes.length > 0) {
+ drawScenicRoutes(scenicRoutes, activeScenicIdx);
+ }
+ // Масштабная линейка карты (риск R3).
+ if (typeof window._updateScaleZoom === 'function') {
+ window._updateScaleZoom();
+ }
+}
+// <<< ET-005 unit toggle block <<<
+
function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
const map = window._map;
if (!map) return;
diff --git a/src/web/index.html b/src/web/index.html
index 2a89e35..b9b8ade 100644
--- a/src/web/index.html
+++ b/src/web/index.html
@@ -58,6 +58,15 @@
POI
+
+
+
+
Единицы
+
+
+
+
+