From 5bb2fa96d70862be577f27ccbf07334277e79e32 Mon Sep 17 00:00:00 2001 From: Slava Date: Sun, 31 May 2026 15:49:26 +0300 Subject: [PATCH 1/8] docs: init ET-007 business request --- docs/work-items/ET-007/00-business-request.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/work-items/ET-007/00-business-request.md diff --git a/docs/work-items/ET-007/00-business-request.md b/docs/work-items/ET-007/00-business-request.md new file mode 100644 index 0000000..545e408 --- /dev/null +++ b/docs/work-items/ET-007/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: ET-005: Спутниковая карта (Схема/Спутник) + +Work Item ID: ET-007 + +## Description + +TBD -- 2.49.1 From d7d06bb0463cffdde9429918aff509663d93e05e Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 31 May 2026 18:28:31 +0000 Subject: [PATCH 2/8] docs(ET-007): analyst artifacts - BRD, TRZ, AC, TestPlan, UI tests --- docs/work-items/ET-007/01-brd.md | 98 +++++ docs/work-items/ET-007/02-trz.md | 385 ++++++++++++++++++ .../ET-007/03-acceptance-criteria.md | 207 ++++++++++ docs/work-items/ET-007/04-test-plan.yaml | 231 +++++++++++ docs/work-items/ET-007/04b-ui-test-cases.md | 274 +++++++++++++ 5 files changed, 1195 insertions(+) create mode 100644 docs/work-items/ET-007/01-brd.md create mode 100644 docs/work-items/ET-007/02-trz.md create mode 100644 docs/work-items/ET-007/03-acceptance-criteria.md create mode 100644 docs/work-items/ET-007/04-test-plan.yaml create mode 100644 docs/work-items/ET-007/04b-ui-test-cases.md diff --git a/docs/work-items/ET-007/01-brd.md b/docs/work-items/ET-007/01-brd.md new file mode 100644 index 0000000..235da43 --- /dev/null +++ b/docs/work-items/ET-007/01-brd.md @@ -0,0 +1,98 @@ +--- +type: brd +work_item_id: ET-007 +title: "BRD: Спутниковая карта (Схема / Спутник)" +version: 1 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +authors: + - "agent:analyst" +--- + +# BRD — ET-007: Спутниковая карта (Схема / Спутник) + +## 1. Цель + +Дать пользователю возможность одним кликом переключать подложку карты +между «Схемой» (текущая OSM-схема) и «Спутник» (растровые снимки +поверхности Земли). Спутниковая подложка помогает увидеть реальный +рельеф и поверхность маршрута — лес/поле/брод/каменистый участок — до +выезда. + +## 2. Контекст + +- Сейчас в приложении используется единственная подложка — OSM-растр, + стилизованный для «Схемы» в двух темах (`style.json`, + `style-dark.json`). Спутникового слоя нет. +- В фазе PH-5 Redesign уже была введена тёмная/светлая тема — но + «тема» относится к стилизации (контрасты, насыщенность), а не к + природе подложки. +- Эндуро-маршруты часто проходят вне дорог OSM (бездорожье, броды, + лесные участки). Спутник критичен для разведки. +- Все клиентские модули (`app.js`, `units.js`, `gpx.js`) уже умеют + переживать `map.setStyle()` через `rebuildMapOverlays()` — это + опорная точка для будущей реализации. + +## 3. Scope + +### In scope + +| # | Функция | +| ----- | ------------------------------------------------------------------------------------ | +| F-01 | Переключатель «Схема / Спутник» в UI (segmented control) | +| F-02 | Спутниковая подложка как новый raster-источник (бесплатный, без API-ключа) | +| F-03 | В режиме «Спутник» — скрыта OSM-схема, показаны спутниковые тайлы | +| F-04 | Все надстройки (грунтовки, тропы, POI, hillshade, TRI, маршрут, GPX) поверх спутника | +| F-05 | Сохранение выбора в `localStorage` (ключ `map-base-layer`) | +| F-06 | Восстановление выбора при загрузке страницы и при смене темы | +| F-07 | Корректное отображение атрибуции спутниковых тайлов | +| F-08 | Сохранение всех пользовательских слоёв (роутинг, GPX, recon) при переключении | + +### Out of scope + +- Кэширование спутниковых тайлов (offline / PWA — это PH-9). +- Динамический выбор провайдера спутниковых тайлов в UI. +- Гибридный режим «Спутник + подписи дорог OSM поверх». +- Самостоятельный хостинг спутниковых тайлов (юридические/трафик-риски). +- Изменение базовой карты для расчёта маршрутов (роутинг по-прежнему OSRM). +- Авто-переключение Схема/Спутник в зависимости от зума. + +## 4. Метрики успеха + +| Метрика | Критерий | +| ------------------------ | ------------------------------------------------------------------------------------- | +| Время переключения | ≤ 500 мс от клика до первой видимой спутниковой плитки | +| Сохранение состояния | Выбор подложки сохраняется после reload, смены темы, смены слоёв terrain | +| Совместимость со слоями | Грунтовки, тропы, POI, маршрут OSRM, GPX-треки, hillshade, TRI видны и поверх спутника | +| Совместимость с темой | Переключение тёмной/светлой темы не сбрасывает режим «Спутник» | +| Атрибуция | На карте видна корректная атрибуция провайдера спутника | +| Не ломает существующее | Все режимы (роутинг, разведка, красивый маршрут, GPX, линейка) работают как прежде | + +## 5. Риски + +| Риск | Вероятность | Влияние | Митигация | +| ------------------------------------------------------------------------------------------------- | ----------- | ------- | -------------------------------------------------------------------------------------------------------------------------- | +| Провайдер спутниковых тайлов закроет доступ / введёт лимит / потребует API-ключ | Средняя | Высокое | Зафиксировать конкретного провайдера в ADR; предусмотреть точку расширения для альтернативного провайдера (несколько URL) | +| Спутниковая подложка медленно грузится → пользователь видит «дыры» | Высокая | Среднее | Использовать background-цвет (тёмно-серый) под спутником; OSM-схема остаётся как fallback в случае ошибки загрузки тайлов | +| Цвет грунтовок и троп плохо виден на спутниковой подложке | Высокая | Среднее | TRZ: на режиме «Спутник» включается обводка (halo) у линий грунтовок и троп — по аналогии с подписями POI | +| Hillshade поверх спутника даёт некрасивое наложение (двойное затенение рельефа) | Средняя | Низкое | По умолчанию hillshade отключается при включении спутника — поведение фиксируется в TRZ | +| Юридические ограничения на использование стороннего провайдера спутниковых тайлов | Низкая | Высокое | В ADR указать выбранного провайдера с лицензией, разрешающей использование без API-ключа (Esri World Imagery, ArcGIS) | +| Регресс UI на мобильных устройствах из-за нового переключателя | Низкая | Среднее | UI-тест-кейсы (04b) для desktop и mobile viewport | +| Конфликт с уже сохранёнными localStorage-значениями старых версий | Низкая | Низкое | Использовать новый ключ `map-base-layer`, default = `schematic` | + +## 6. Зависимости + +- Только фронтенд — backend изменений не требуется. +- MapLibre GL JS 4.7.0 (уже подключен). +- Внешний провайдер спутниковых тайлов (выбор и фиксация — в ADR). +- Сетевое подключение клиента к серверу провайдера. + +## 7. Связь с roadmap + +- Фаза PH-5 Redesign — тёмная тема и mobile UI уже сделаны; ET-007 + встраивается в эту же панель «Рельеф / Слои» (одна точка управления + визуальными слоями карты). +- Фаза PH-9 PWA — кэширование спутниковых тайлов оффлайн — будет + планироваться отдельно, ET-007 закладывает архитектурную основу + (источник тайлов, точка переключения). diff --git a/docs/work-items/ET-007/02-trz.md b/docs/work-items/ET-007/02-trz.md new file mode 100644 index 0000000..ea6eb40 --- /dev/null +++ b/docs/work-items/ET-007/02-trz.md @@ -0,0 +1,385 @@ +--- +type: trz +work_item_id: ET-007 +title: "ТЗ: Спутниковая карта (Схема / Спутник)" +version: 1 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +authors: + - "agent:analyst" +--- + +# ТЗ — ET-007: Спутниковая карта (Схема / Спутник) + +## 1. Функциональные требования + +### REQ-F-01: Переключатель «Схема / Спутник» + +- В попап-панели слоёв (`#terrain-popup`, открывается кнопкой + `#terrain-toggle`) добавляется новая секция в самом верху панели — + «Подложка». +- Реализация — segmented-control (`.seg-control` / `.seg-btn`) с двумя + кнопками: + - «Схема» (`data-base="schematic"`, ID `base-btn-schematic`) — + активна по умолчанию. + - «Спутник» (`data-base="satellite"`, ID `base-btn-satellite`). +- Активная кнопка визуально выделяется (`.active` — оранжевый фон, по + аналогии с переключателем единиц измерения, ET-005). +- Обработчик: `onBaseLayerToggle(base)` в `src/web/app.js`. +- Под переключателем — горизонтальная линия-разделитель (`
`), + как уже сделано между секциями попапа. + +### REQ-F-02: Спутниковый растровый источник + +- Используется растровый тайл-сервер Esri World Imagery (см. ADR в + `docs/work-items/ET-007/06-adr/`): + - URL-шаблон: `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}`. + - `tileSize: 256`, `minzoom: 0`, `maxzoom: 19`. + - Атрибуция: «Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community». +- Источник добавляется на карту лениво: при первом включении режима + «Спутник», а не на старте приложения. +- ID источника: `satellite-raster`. +- ID слоя: `satellite-base`. + +### REQ-F-03: Поведение в режиме «Спутник» + +- При включении «Спутник»: + - Если ещё не добавлен — добавить source `satellite-raster` и layer + `satellite-base` сразу после слоя `background` (т.е. ниже всех + остальных слоёв). + - Слой `osm-base` (существующий) скрывается (`visibility: none`). + - Слой `background` остаётся (показывает «дыры» если тайлы ещё не + загрузились) — цвет фона `#2a2a2a` для тёмной темы и `#1a1a1a` для + светлой темы в режиме «Спутник» (чтобы белый фон не «бликовал» под + тёмными снимками). +- При возврате на «Схема»: + - `osm-base` снова видим (`visibility: visible`). + - `satellite-base` скрывается (`visibility: none`), но не удаляется + из стиля (быстрое повторное переключение). + +### REQ-F-04: Совместимость со слоями приложения + +Все клиентские слои должны корректно отображаться поверх спутника: + +| Слой | Z-order над спутником | Доп. правила в режиме «Спутник» | +| ----------------------------- | --------------------- | ------------------------------------------------------------------------------ | +| Hillshade (`terrain-hillshade`) | поверх спутника | Включается/выключается чекбоксом как раньше; по умолчанию НЕ авто-выключается | +| TRI (`terrain-tri`) | поверх спутника | Аналогично hillshade | +| Trails (grade1..5) | поверх terrain | Линия получает halo (line-gap-width + полупрозрачная обводка) для контраста | +| Paths/bridleway | поверх trails | Аналогично — halo для контраста | +| POI circles | поверх trails | Обводка `circle-stroke-color: #ffffff`, толщина 2 px | +| POI labels | поверх POI | `text-halo-color: #000000`, `text-halo-width: 2px` для читаемости на спутнике | +| Route / Scenic / Link / Ruler | поверх POI | Без изменений | +| GPX-треки и waypoints | поверх Route | Без изменений (ET-006 уже совместим) | + +Реализация: +- Для halo у линий грунтовок/троп добавить отдельные «underlay»-слои с + более широкой полупрозрачной белой линией; включать их через + `visibility` только в режиме «Спутник». +- Стили POI на спутнике задаются динамически через `setPaintProperty` + при переключении режима. + +### REQ-F-05: Сохранение состояния (localStorage) + +- Ключ: `map-base-layer`. +- Значения: `"schematic"` (default) | `"satellite"`. +- При `onBaseLayerToggle()` — запись. +- При старте приложения — чтение и применение через + `restoreBaseLayerState()` (по аналогии с `restoreTerrainState()`). + +### REQ-F-06: Восстановление после смены стиля карты + +- При вызове `map.setStyle()` (переключение тёмной/светлой темы, см. + `switchMapStyle()` в `app.js`) спутниковый source/layer удаляются + вместе со стилем. +- В функции `rebuildMapOverlays()` добавляется вызов + `restoreBaseLayerState()` — это пересоздаёт source/layer спутника и + выставляет видимость по сохранённому состоянию. +- Порядок вызовов в `rebuildMapOverlays()`: `restoreBaseLayerState()` + вызывается **до** `restoreTerrainState()` — чтобы hillshade/TRI + оказались выше спутника, но ниже trails (тот же подход, что и для + schematic-режима). + +### REQ-F-07: Атрибуция + +- При создании source `satellite-raster` передаётся свойство + `attribution: "Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community"`. +- MapLibre автоматически отображает атрибуцию в правом нижнем углу + карты, когда соответствующий source активен. +- Атрибуция OSM остаётся видимой в обоих режимах (vector-источник + `trails-tiles` всегда активен). + +### REQ-F-08: Fallback при ошибке загрузки тайлов + +- Если спутниковые тайлы не загружаются (network error / 4xx / 5xx), + MapLibre сам показывает прозрачную плитку — под ней видим `background`. +- Логика fallback на схему не предусмотрена (пользователь сам + переключит, если нужно). + +## 2. Нефункциональные требования + +### REQ-NF-01: Производительность + +- Время переключения «Схема → Спутник» (до первой видимой спутниковой + плитки): ≤ 500 мс при скорости сети ≥ 5 Мбит/с. +- Переключение обратно «Спутник → Схема» — мгновенное (источник + остаётся в стиле, меняется только visibility). +- В момент переключения не должно быть «прыжков» камеры — `center`, + `zoom`, `bearing`, `pitch` сохраняются. + +### REQ-NF-02: Совместимость + +- Браузеры: Chrome 90+, Firefox 90+, Safari 15+. +- Мобильные: iOS Safari 15+, Chrome для Android. +- MapLibre GL JS 4.7.0 (уже подключен). + +### REQ-NF-03: UX + +- Текущая активная подложка визуально видна в UI всегда (в попапе + слоёв). +- Переключение происходит без перезагрузки страницы и без потери + пользовательского состояния (маршрута, GPX, точек разведки). + +### REQ-NF-04: Хранение + +- localStorage ключ `map-base-layer`, размер ≤ 16 байт. +- Никаких других данных приложение для этой фичи не хранит. + +### REQ-NF-05: Безопасность + +- Запросы к Esri World Imagery идут по HTTPS. +- Никаких персональных данных пользователя в URL запросов не + передаётся. +- Атрибуция выводится в соответствии с лицензией провайдера (см. ADR). + +## 3. UI-спецификация + +### 3.1 Изменения в `#terrain-popup` + +Сейчас: +``` +┌────────────────────────────┐ +│ Эндуро │ +│ ☐ Тени рельефа │ +│ ☐ Перепады │ +│ ─────── │ +│ ☑ Грунтовки │ +│ ☑ Тропы │ +│ ─────── │ +│ ☑ POI │ +│ ─────── │ +│ Единицы [км][мили] │ +└────────────────────────────┘ +``` + +После: +``` +┌────────────────────────────┐ +│ Подложка [Схема][Спутник] │ ← новая секция +│ ─────── │ +│ Эндуро │ +│ ☐ Тени рельефа │ +│ ☐ Перепады │ +│ ─────── │ +│ ☑ Грунтовки │ +│ ☑ Тропы │ +│ ─────── │ +│ ☑ POI │ +│ ─────── │ +│ Единицы [км][мили] │ +└────────────────────────────┘ +``` + +### 3.2 Разметка HTML + +В `src/web/index.html`, в начале `#terrain-popup` (сразу после +`
Эндуро
` ИЛИ выше него — по +выбору разработчика; рекомендуется в самом верху для большей +заметности): + +```html + +
+ Подложка +
+ + +
+
+
+``` + +### 3.3 CSS + +В `src/web/app.css` — добавить стили (по аналогии с `.terrain-unit-row`): + +```css +.terrain-base-row { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; +} +.terrain-base-label { + font-size: 12px; + color: var(--text2); + flex-shrink: 0; +} +.terrain-base-row .seg-control { + flex: 1; + margin-bottom: 0; +} +.base-seg .seg-btn { + font-size: 12px; +} +``` + +### 3.4 Поведение на мобильных устройствах + +- Попап `#terrain-popup` уже адаптирован под мобильные (ET-005). Новая + строка не должна нарушать ширину попапа. +- Высота кнопок `.seg-btn` остаётся 34px (как у переключателя единиц). + +## 4. Данные + +### 4.1 Спутниковый источник (MapLibre source spec) + +```js +{ + type: 'raster', + tiles: [ + 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}' + ], + tileSize: 256, + minzoom: 0, + maxzoom: 19, + attribution: 'Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community' +} +``` + +### 4.2 Спутниковый слой (MapLibre layer spec) + +```js +{ + id: 'satellite-base', + type: 'raster', + source: 'satellite-raster', + paint: { + 'raster-opacity': 1.0, + 'raster-resampling': 'linear' + }, + layout: { visibility: 'none' } // включается при переключении +} +``` + +Вставляется в стиль сразу после слоя `background`. + +### 4.3 localStorage + +| Ключ | Значения | Default | +| ----------------- | ------------------------------ | ------------- | +| `map-base-layer` | `"schematic"` \| `"satellite"` | `"schematic"` | + +## 5. Алгоритмы + +### 5.1 `onBaseLayerToggle(base)` + +``` +1. Если base === текущий — return. +2. Сохранить в localStorage('map-base-layer', base). +3. Применить applyBaseLayer(base). +4. syncBaseLayerUI(base). +``` + +### 5.2 `applyBaseLayer(base)` + +``` +1. map = window._map; если нет — return. +2. Если base === 'satellite': + 2.1. Если source 'satellite-raster' отсутствует — addSource (см. 4.1). + 2.2. Если layer 'satellite-base' отсутствует — addLayer (см. 4.2), + вставлять beforeId = id первого слоя trails-* или terrain-* + (первый из существующих) — чтобы спутник оказался под terrain + и trails. + 2.3. setLayoutProperty('satellite-base', 'visibility', 'visible'). + 2.4. setLayoutProperty('osm-base', 'visibility', 'none'). + 2.5. Применить «спутниковые» правки к слоям trails/path/poi: + - усилить halo у line-слоёв (через setPaintProperty); + - сделать POI text-halo чёрным. + 2.6. Сменить background-color на тёмно-серый (#2a2a2a). +3. Иначе (base === 'schematic'): + 3.1. setLayoutProperty('osm-base', 'visibility', 'visible'). + 3.2. setLayoutProperty('satellite-base', 'visibility', 'none') + (если слой существует). + 3.3. Вернуть halo trails / POI к дефолтным значениям из текущего стиля. + 3.4. Background-color — из исходного стиля (не трогать, + он восстанавливается при setStyle). +``` + +### 5.3 `restoreBaseLayerState()` + +``` +1. base = localStorage.getItem('map-base-layer') || 'schematic'. +2. syncBaseLayerUI(base). +3. applyBaseLayer(base). +``` + +### 5.4 `syncBaseLayerUI(base)` + +``` +1. schematicBtn.classList.toggle('active', base === 'schematic'). +2. satelliteBtn.classList.toggle('active', base === 'satellite'). +``` + +### 5.5 Интеграция с `rebuildMapOverlays()` (`app.js`) + +В существующей функции (см. `app.js`, ~строка 127) добавить вызов +**первым**: + +```js +function rebuildMapOverlays() { + // ET-007: восстановить выбранную подложку первой — + // чтобы terrain/trails/POI применили свои overlays поверх неё + if (typeof restoreBaseLayerState === 'function') { + restoreBaseLayerState(); + } + // ── далее без изменений ── + restoreTerrainState(); + restoreTrailsState(); + // ... +} +``` + +## 6. Файловая структура изменений + +``` +src/web/ +├── index.html # + блок переключателя в #terrain-popup +├── app.css # + стили .terrain-base-row, .base-seg +├── app.js # + onBaseLayerToggle, applyBaseLayer, + # restoreBaseLayerState, syncBaseLayerUI, + # правка rebuildMapOverlays +``` + +Backend изменений нет. + +## 7. Взаимодействие с существующими режимами + +- Все режимы тулбара (Маршрут, Связка, Красивый, Разведка, Линейка, + Поиск, Метка, GPX) работают независимо от выбранной подложки. +- Переключение подложки **не сбрасывает** состояние режимов: маршруты, + GPX-треки, точки разведки, линейка, метки — остаются. +- Переключение темы (тёмная/светлая) **не сбрасывает** выбор подложки. +- При вызове `map.setStyle()` (тема, восстановление стиля) + спутниковый слой пересоздаётся в `rebuildMapOverlays()`. + +## 8. Открытые вопросы для ADR + +- Выбор провайдера спутниковых тайлов (Esri / Mapbox / Bing / OpenAerialMap). +- Решение по halo для POI/trails на спутнике: статические правки в + `style.json` через `visibility` или динамические `setPaintProperty`. +- Поведение hillshade при включении спутника: оставить как есть (по + выбору пользователя) — зафиксировано в REQ-F-04 как «оставить». diff --git a/docs/work-items/ET-007/03-acceptance-criteria.md b/docs/work-items/ET-007/03-acceptance-criteria.md new file mode 100644 index 0000000..cece277 --- /dev/null +++ b/docs/work-items/ET-007/03-acceptance-criteria.md @@ -0,0 +1,207 @@ +--- +type: acceptance-criteria +work_item_id: ET-007 +title: "AC: Спутниковая карта (Схема / Спутник)" +version: 1 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +authors: + - "agent:analyst" +--- + +# Acceptance Criteria — ET-007: Спутниковая карта (Схема / Спутник) + +## AC-01: UI переключателя + +```gherkin +Feature: Переключатель подложки в попапе слоёв + + Scenario: Открытие попапа показывает переключатель + Given пользователь находится на карте + When пользователь нажимает кнопку «Рельеф» (#terrain-toggle) + Then открывается попап #terrain-popup + And в попапе виден segmented-control «Подложка» с кнопками «Схема» и «Спутник» + And одна из кнопок имеет класс .active + + Scenario: Default — Схема + Given localStorage пуст (или ключ 'map-base-layer' не задан) + When пользователь открывает попап слоёв + Then активна кнопка «Схема» (#base-btn-schematic) + And не активна кнопка «Спутник» (#base-btn-satellite) +``` + +## AC-02: Переключение на «Спутник» + +```gherkin +Feature: Переключение Схема → Спутник + + Scenario: Базовое переключение + Given активна подложка «Схема» + When пользователь нажимает «Спутник» в попапе слоёв + Then кнопка «Спутник» получает класс .active + And кнопка «Схема» теряет класс .active + And на карте слой osm-base скрыт (visibility=none) + And на карте появляется слой satellite-base (visibility=visible) + And положение карты (center, zoom, bearing, pitch) не изменилось + + Scenario: Атрибуция Esri отображается + Given пользователь включил режим «Спутник» + Then в нижнем правом углу карты видна атрибуция «Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community» +``` + +## AC-03: Переключение на «Схема» + +```gherkin +Feature: Переключение Спутник → Схема + + Scenario: Возврат на схему + Given активна подложка «Спутник» + When пользователь нажимает «Схема» в попапе слоёв + Then кнопка «Схема» получает класс .active + And слой osm-base снова виден (visibility=visible) + And слой satellite-base скрыт (visibility=none), но source остаётся в стиле + And положение карты не изменилось +``` + +## AC-04: Совместимость со слоями приложения + +```gherkin +Feature: Слои поверх спутника + + Scenario: Грунтовки и тропы видны на спутнике + Given активна подложка «Спутник» + And в попапе включены «Грунтовки» и «Тропы» + Then на карте видны линии грунтовок и троп поверх спутника + And линии имеют визуально различимую обводку (halo) для контраста + + Scenario: POI видны на спутнике + Given активна подложка «Спутник» + And в попапе включён «POI» + Then на карте видны маркеры POI поверх спутника + And подписи POI читаемы (имеют тёмный halo) + + Scenario: Hillshade поверх спутника + Given активна подложка «Спутник» + When пользователь включает «Тени рельефа» + Then на карте видны и спутник, и hillshade (hillshade поверх спутника) + + Scenario: Маршрут OSRM поверх спутника + Given пользователь построил маршрут через OSRM + When пользователь переключает подложку на «Спутник» + Then маршрут остаётся виден поверх спутника + And статистика маршрута сохранена + + Scenario: GPX-треки поверх спутника + Given пользователь загрузил GPX-трек + When пользователь переключает подложку на «Спутник» + Then GPX-линии и waypoints остаются видны поверх спутника +``` + +## AC-05: Сохранение в localStorage + +```gherkin +Feature: Persistence выбора подложки + + Scenario: Сохранение при переключении + Given активна подложка «Схема» + When пользователь нажимает «Спутник» + Then localStorage['map-base-layer'] === 'satellite' + + Scenario: Восстановление после reload + Given localStorage['map-base-layer'] === 'satellite' + When пользователь перезагружает страницу + Then после загрузки карты активна подложка «Спутник» + And кнопка «Спутник» имеет класс .active +``` + +## AC-06: Восстановление при смене темы + +```gherkin +Feature: Подложка переживает смену темы + + Scenario: Переключение тёмной/светлой темы в режиме «Спутник» + Given активна подложка «Спутник» + When пользователь переключает тему (тёмная ↔ светлая) + Then после завершения map.setStyle() спутниковый слой восстановлен + And подложка «Спутник» остаётся активной + And все слои поверх (trails, POI, маршрут, GPX) восстановлены + + Scenario: Переключение слоёв terrain в режиме «Спутник» + Given активна подложка «Спутник» + When пользователь включает или выключает «Тени рельефа» / «Перепады» + Then подложка «Спутник» остаётся активной +``` + +## AC-07: Совместимость с режимами тулбара + +```gherkin +Feature: Подложка не мешает другим режимам + + Scenario: Режим «Маршрут» на спутнике + Given активна подложка «Спутник» + When пользователь активирует режим «Маршрут» + And тапает 2 точки на карте + Then маршрут строится корректно + And линия маршрута видна на спутнике + + Scenario: Режим «Разведка» на спутнике + Given активна подложка «Спутник» + When пользователь активирует режим «Разведка» и тапает на карту + Then круг радиуса разведки видим + And статистика разведки отображается + + Scenario: Линейка на спутнике + Given активна подложка «Спутник» + When пользователь активирует «Линейка» и расставляет точки + Then линия линейки видна + And расстояние отображается + + Scenario: Поиск на спутнике + Given активна подложка «Спутник» + When пользователь нажимает «Поиск» и вводит запрос + Then результаты поиска отображаются + And карта корректно центрируется на найденной точке +``` + +## AC-08: Производительность + +```gherkin +Feature: Скорость переключения + + Scenario: Переключение Схема → Спутник + Given активна подложка «Схема» и сеть ≥ 5 Мбит/с + When пользователь нажимает «Спутник» + Then первая спутниковая плитка отображается в течение ≤ 500 мс + + Scenario: Переключение Спутник → Схема + Given активна подложка «Спутник» (тайлы уже подгружены) + When пользователь нажимает «Схема» + Then смена визуально мгновенная (≤ 100 мс) +``` + +## AC-09: Mobile UI + +```gherkin +Feature: Переключатель на мобильных устройствах + + Scenario: Попап слоёв на мобильном + Given пользователь открыл приложение на мобильном устройстве (виртуальный viewport 375×812) + When пользователь открывает попап слоёв + Then переключатель «Подложка» виден полностью + And обе кнопки нажимаемы (touch target ≥ 34px) + And не перекрывает другие элементы попапа +``` + +## AC-10: Не ломает существующий функционал + +```gherkin +Feature: Регресс-проверка + + Scenario: Все режимы работают как в режиме «Схема», так и в «Спутник» + Given пользователь использует приложение + Then режимы Маршрут, Связка, Красивый, Разведка, Линейка, Поиск, Метка, GPX + работают одинаково в обеих подложках + And переключение единиц измерения (км/мили) работает в обеих подложках + And переключение темы работает в обеих подложках +``` diff --git a/docs/work-items/ET-007/04-test-plan.yaml b/docs/work-items/ET-007/04-test-plan.yaml new file mode 100644 index 0000000..a5ef729 --- /dev/null +++ b/docs/work-items/ET-007/04-test-plan.yaml @@ -0,0 +1,231 @@ +--- +type: test-plan +work_item_id: ET-007 +title: "Test Plan: Спутниковая карта (Схема / Спутник)" +version: 1 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +authors: + - "agent:analyst" + +test_suites: + + - name: unit-base-layer-state + type: unit + description: "Чтение/запись/восстановление выбора подложки" + cases: + - id: U-01 + name: "Default — Схема, если localStorage пуст" + input: "localStorage без ключа 'map-base-layer'" + expected: "restoreBaseLayerState() выставляет base='schematic'" + + - id: U-02 + name: "Чтение значения 'satellite' из localStorage" + input: "localStorage['map-base-layer'] = 'satellite'" + expected: "restoreBaseLayerState() выставляет base='satellite'" + + - id: U-03 + name: "Запись значения при переключении" + input: "onBaseLayerToggle('satellite')" + expected: "localStorage['map-base-layer'] === 'satellite'" + + - id: U-04 + name: "Игнор некорректного значения в localStorage" + input: "localStorage['map-base-layer'] = 'unknown'" + expected: "restoreBaseLayerState() fallback на 'schematic'" + + - id: U-05 + name: "Toggle на уже активный режим — no-op" + input: "active=schematic; onBaseLayerToggle('schematic')" + expected: "Никаких изменений в стиле, localStorage не записывается повторно" + + - name: unit-ui-sync + type: unit + description: "Синхронизация .active у кнопок переключателя" + cases: + - id: U-10 + name: "syncBaseLayerUI('satellite')" + input: "DOM с #base-btn-schematic.active и #base-btn-satellite без класса" + expected: "После: #base-btn-satellite.active=true, #base-btn-schematic.active=false" + + - id: U-11 + name: "syncBaseLayerUI('schematic')" + input: "DOM с #base-btn-satellite.active" + expected: "После: #base-btn-schematic.active=true, #base-btn-satellite.active=false" + + - name: integration-maplibre-layers + type: integration + description: "Взаимодействие с MapLibre source/layer" + cases: + - id: I-01 + name: "Добавление спутникового source при первом включении" + input: "applyBaseLayer('satellite') впервые" + expected: "map.getSource('satellite-raster') !== undefined; URL содержит arcgisonline.com" + + - id: I-02 + name: "Добавление спутникового layer при первом включении" + input: "applyBaseLayer('satellite') впервые" + expected: "map.getLayer('satellite-base') !== undefined; type='raster'" + + - id: I-03 + name: "Visibility OSM-base после переключения на спутник" + input: "applyBaseLayer('satellite')" + expected: "map.getLayoutProperty('osm-base', 'visibility') === 'none'" + + - id: I-04 + name: "Visibility satellite-base после переключения на схему" + input: "applyBaseLayer('satellite') → applyBaseLayer('schematic')" + expected: "satellite-base.visibility==='none', osm-base.visibility==='visible'" + + - id: I-05 + name: "Z-order: satellite ниже terrain и trails" + input: "applyBaseLayer('satellite'); включены hillshade и trails" + expected: "Layer index(satellite-base) < index(terrain-hillshade) < index(trails-track)" + + - id: I-06 + name: "Position карты сохраняется при переключении" + input: "center=[37.6,55.75], zoom=10; applyBaseLayer('satellite')" + expected: "После: getCenter() == [37.6,55.75], getZoom() == 10" + + - id: I-07 + name: "Атрибуция Esri зарегистрирована" + input: "applyBaseLayer('satellite')" + expected: "source 'satellite-raster' содержит attribution с упоминанием Esri" + + - name: integration-style-switch + type: integration + description: "Поведение при map.setStyle (смена темы)" + cases: + - id: I-10 + name: "Спутник восстанавливается после setStyle (тёмная → светлая)" + input: "active='satellite'; вызывается switchMapStyle()" + expected: "После idle: layer 'satellite-base' существует; visibility='visible'; osm-base.visibility='none'" + + - id: I-11 + name: "Сохранённое состояние читается из localStorage в rebuildMapOverlays" + input: "localStorage='satellite'; rebuildMapOverlays() вручную" + expected: "applyBaseLayer вызван с 'satellite'" + + - id: I-12 + name: "Восстановление выполняется до restoreTerrainState" + input: "rebuildMapOverlays() с заглушками-shpions" + expected: "Порядок вызовов: restoreBaseLayerState → restoreTerrainState" + + - name: integration-other-layers + type: integration + description: "Совместимость со всеми клиентскими слоями" + cases: + - id: I-20 + name: "Маршрут OSRM не теряется при переключении" + input: "Построен маршрут; applyBaseLayer('satellite')" + expected: "Layer маршрута существует, координаты не изменились" + + - id: I-21 + name: "GPX-трек не теряется при переключении" + input: "Загружен GPX; applyBaseLayer('satellite')" + expected: "Layer gpx-* существует, source.data не изменён" + + - id: I-22 + name: "Recon-круг не теряется при переключении" + input: "Активен recon; applyBaseLayer('satellite')" + expected: "Recon-круг отображается на карте" + + - id: I-23 + name: "Hillshade поверх спутника" + input: "applyBaseLayer('satellite'); включить hillshade" + expected: "Оба слоя видимы; hillshade выше satellite-base в стиле" + + - id: I-24 + name: "POI halo чёрный на спутнике" + input: "applyBaseLayer('satellite')" + expected: "map.getPaintProperty('poi-labels','text-halo-color') === '#000000' (или эквивалент)" + + - id: I-25 + name: "POI halo дефолтный на схеме" + input: "applyBaseLayer('schematic') после спутника" + expected: "POI labels вернули halo цвет схемы (#ffffff)" + + - name: e2e-base-layer-workflow + type: e2e + description: "Полный пользовательский сценарий" + cases: + - id: E-01 + name: "Открыть попап → включить спутник → сохранилось" + steps: + - "Открыть приложение (default — Схема)" + - "Нажать кнопку «Рельеф» в правой панели" + - "Убедиться: переключатель «Подложка» виден" + - "Нажать «Спутник»" + - "Убедиться: спутниковые тайлы загрузились" + - "Убедиться: атрибуция Esri видна" + - "Перезагрузить страницу" + - "Убедиться: после загрузки активен «Спутник»" + + - id: E-02 + name: "Переключение туда-обратно без потери маршрута" + steps: + - "Построить маршрут через OSRM (2 точки)" + - "Переключить на «Спутник»" + - "Убедиться: маршрут виден на спутнике, статистика сохранена" + - "Переключить на «Схема»" + - "Убедиться: маршрут виден на схеме, статистика та же" + + - id: E-03 + name: "Спутник + загрузка GPX" + steps: + - "Переключить на «Спутник»" + - "Загрузить GPX-файл" + - "Убедиться: трек отрисован поверх спутника" + - "Убедиться: цвет трека различим" + + - id: E-04 + name: "Спутник + смена темы" + steps: + - "Переключить на «Спутник»" + - "Переключить тёмную тему на светлую" + - "Дождаться idle" + - "Убедиться: подложка осталась «Спутник»" + - "Убедиться: все остальные слои восстановились" + + - id: E-05 + name: "Спутник + переключение единиц измерения" + steps: + - "Переключить на «Спутник»" + - "Открыть попап слоёв и переключить «мили»" + - "Убедиться: единицы переключились, подложка не сбросилась" + + - id: E-06 + name: "Спутник + hillshade" + steps: + - "Переключить на «Спутник»" + - "Включить «Тени рельефа»" + - "Убедиться: видны спутник и тени одновременно" + + - id: E-07 + name: "Линейка на спутнике" + steps: + - "Переключить на «Спутник»" + - "Активировать линейку" + - "Поставить 3 точки на карте" + - "Убедиться: линия линейки видна на спутнике" + - "Убедиться: расстояния отображаются" + + - name: e2e-error-handling + type: e2e + description: "Поведение при сетевых ошибках" + cases: + - id: E-10 + name: "Спутниковые тайлы недоступны (offline)" + steps: + - "Включить «Спутник»" + - "Симулировать offline (DevTools throttling: Offline)" + - "Сдвинуть карту в новую область" + - "Убедиться: приложение не падает; видим фон background" + - "Восстановить сеть → тайлы догружаются" + +test_data: + - name: "test-track-simple.gpx" + description: "1 трек, 10 точек — для проверки совместимости с GPX" + - name: "Тестовый OSRM-маршрут" + description: "2 waypoint в районе [37.6,55.75] → [37.7,55.8]" diff --git a/docs/work-items/ET-007/04b-ui-test-cases.md b/docs/work-items/ET-007/04b-ui-test-cases.md new file mode 100644 index 0000000..497f857 --- /dev/null +++ b/docs/work-items/ET-007/04b-ui-test-cases.md @@ -0,0 +1,274 @@ +--- +type: ui-test-cases +work_item_id: ET-007 +title: "UI Test Cases: Спутниковая карта (Схема / Спутник)" +version: 1 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +authors: + - "agent:analyst" +--- + +# UI Test Cases — ET-007: Спутниковая карта (Схема / Спутник) + +Базовый URL: `https://openclaw.mva154.duckdns.org/enduro/` + +Все тесты проверяют появление и поведение переключателя «Подложка» в +попапе слоёв, а также корректное отображение спутниковой подложки +поверх существующих UI-элементов. + +--- + +### TC-UI-01 — Переключатель «Подложка» виден в попапе + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. screenshot: "01-popup-with-base-toggle" +6. check-visual: "В открывшемся попапе #terrain-popup видна строка «Подложка» с двумя кнопками: «Схема» (активна, оранжевый фон) и «Спутник» (неактивна)" + +--- + +### TC-UI-02 — Активация «Спутник» меняет подложку + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. screenshot: "02-satellite-active" +8. check-visual: "Карта показывает спутниковые снимки (зелёные/коричневые поля, реальный рельеф). В попапе кнопка «Спутник» подсвечена оранжевым, «Схема» — нет" + +--- + +### TC-UI-03 — Атрибуция Esri видна + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. click: "#terrain-toggle" +8. wait: 500 +9. screenshot: "03-attribution-esri" +10. check-visual: "В правом нижнем углу карты видна атрибуция со словом «Esri» (или иконка info, при клике на которую разворачивается полный текст)" + +--- + +### TC-UI-04 — Возврат на «Схема» + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. click: "#base-btn-schematic" +8. wait: 2000 +9. screenshot: "04-schematic-restored" +10. check-visual: "Карта снова показывает схему OSM (бежевый/серый фон, дороги). В попапе кнопка «Схема» подсвечена оранжевым" + +--- + +### TC-UI-05 — Грунтовки и тропы видны на спутнике + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. screenshot: "05-trails-on-satellite" +8. check-visual: "На спутниковой подложке отчётливо видны линии грунтовок (золотые/красные) и троп (красные пунктирные). Линии имеют светлую обводку (halo) для контраста с тёмным спутником" + +--- + +### TC-UI-06 — POI и подписи на спутнике читаемы + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. screenshot: "06-poi-on-satellite" +8. check-visual: "POI-маркеры (цветные кружки) видны на спутнике. Подписи POI имеют тёмный halo, читаемы на любом фоне" + +--- + +### TC-UI-07 — Спутник переживает смену темы + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. click: "#btn-theme" +8. wait: 3000 +9. screenshot: "07-satellite-after-theme-switch" +10. check-visual: "После переключения темы карта по-прежнему показывает спутниковую подложку (а не схему)" + +--- + +### TC-UI-08 — Hillshade поверх спутника + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. click: "#terrain-hillshade-cb" +8. wait: 3000 +9. screenshot: "08-hillshade-on-satellite" +10. check-visual: "Виден спутник + затенение рельефа поверх (тёмные тени по склонам, рельеф «выпуклый»). Слои не перекрывают друг друга полностью" + +--- + +### TC-UI-09 — Маршрут OSRM на спутнике + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#tb-route" +4. wait: 1000 +5. click: "#map" +6. wait: 2000 +7. scroll: 100 +8. click: "#map" +9. wait: 5000 +10. click: "#terrain-toggle" +11. wait: 500 +12. click: "#base-btn-satellite" +13. wait: 5000 +14. screenshot: "09-route-on-satellite" +15. check-visual: "Маршрут (синяя/оранжевая линия) виден поверх спутниковой подложки, конечные точки маршрута отмечены маркерами" + +--- + +### TC-UI-10 — Переключатель на мобильном + +- тип: ui +- viewport: mobile + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. screenshot: "10-popup-mobile" +6. check-visual: "На мобильном viewport попап #terrain-popup помещается на экране целиком. Переключатель «Подложка» виден, обе кнопки нажимаемы, не перекрывают другие элементы попапа" + +--- + +### TC-UI-11 — Активация «Спутник» на мобильном + +- тип: ui +- viewport: mobile + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. screenshot: "11-satellite-mobile" +8. check-visual: "Спутниковая подложка отображается на мобильном устройстве. Тулбар внизу и попап работают корректно, переключатель «Спутник» подсвечен" + +--- + +### TC-UI-12 — Persistence: спутник после перезагрузки + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. navigate: https://openclaw.mva154.duckdns.org/enduro/ +8. wait: 5000 +9. screenshot: "12-satellite-after-reload" +10. check-visual: "После перезагрузки карта сразу открывается со спутниковой подложкой (не со схемой). Активный режим — «Спутник»" + +--- + +### TC-UI-13 — GPX-панель + Спутник + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. click: "#tb-gpx" +8. wait: 1000 +9. screenshot: "13-gpx-sheet-on-satellite" +10. check-visual: "Открылась панель #sheet-gpx с пустым состоянием поверх спутниковой карты. Панель и подложка визуально не конфликтуют" + +--- + +### TC-UI-14 — Совместимость с переключателем единиц + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#base-btn-satellite" +6. wait: 5000 +7. click: "#unit-btn-mi" +8. wait: 1000 +9. screenshot: "14-satellite-with-miles" +10. check-visual: "Подложка остаётся «Спутник», единицы переключены на мили (кнопка «мили» подсвечена). Оба переключателя видны и работают независимо" -- 2.49.1 From 231c99c045dc5e247cd1a9e99a91c80fc46dcf4e Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 31 May 2026 20:01:06 +0000 Subject: [PATCH 3/8] docs(ET-007): architecture - ADR, infra-requirements, data-requirements, tech-risks --- docs/architecture/README.md | 15 +- docs/architecture/adr/README.md | 1 + .../06-adr/ADR-004-satellite-base-layer.md | 305 ++++++++++++++++++ .../ET-007/07-infra-requirements.md | 163 ++++++++++ .../work-items/ET-007/08-data-requirements.md | 135 ++++++++ docs/work-items/ET-007/10-tech-risks.md | 200 ++++++++++++ 6 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md create mode 100644 docs/work-items/ET-007/07-infra-requirements.md create mode 100644 docs/work-items/ET-007/08-data-requirements.md create mode 100644 docs/work-items/ET-007/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index aabaab8..893e5dd 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -11,12 +11,25 @@ - **Database** — SQLite + Spatialite (точки интереса, маршруты) ## Слои карты -- Base map (OpenStreetMap) +- Base map: **Схема** (OpenStreetMap raster) либо **Спутник** (Esri World Imagery raster) — переключается в UI (ET-007 / ADR-004) - Hillshade (рельеф с тенями) - TRI (Terrain Ruggedness Index — сложность рельефа) - Hypsometric (высотная раскраска) - Trails (маршруты из OSM) +## Внешние тайл-провайдеры + +Клиент (браузер) обращается напрямую к двум внешним raster-tile сервисам. +Сервер mva154 эти тайлы не проксирует и не кэширует. + +| Провайдер | Назначение | URL | Активация | API-ключ | +|-----------|-----------|-----|-----------|----------| +| OpenStreetMap | Базовый слой «Схема» | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` | всегда (default подложка) | нет | +| Esri World Imagery | Базовый слой «Спутник» | `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` | лениво — только при включении «Спутник» пользователем (ET-007) | нет | + +Атрибуция обоих провайдеров выводится MapLibre автоматически при +активном source. + ## Деплой Один Docker Compose на mva154. Nginx проксирует /enduro/ на контейнер. diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 51da402..c91bad5 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -7,3 +7,4 @@ | ADR-001 | Блокировка шлагбаумов через `mode.inaccessible` | accepted | 2026-05-15 | [ET-001](../../work-items/ET-001/06-adr/ADR-001-barrier-blocking.md) | | ADR-002 | GPX-фича как отдельный модуль `gpx.js` | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-002-gpx-module-structure.md) | | ADR-003 | Парсинг GPX — `DOMParser` в основном потоке с чанковой конвертацией | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-003-gpx-parsing-strategy.md) | +| ADR-004 | Спутниковая подложка: Esri World Imagery, ленивый raster-source, гибридное halo | accepted | 2026-05-31 | [ET-007](../../work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md) | diff --git a/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md b/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md new file mode 100644 index 0000000..37dea4e --- /dev/null +++ b/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md @@ -0,0 +1,305 @@ +--- +type: adr +work_item_id: ET-007 +adr_id: ADR-004 +title: "ADR-004: Спутниковая подложка — провайдер Esri World Imagery, лениво-добавляемый raster-source, гибридная стратегия halo" +status: accepted +created_at: 2026-05-31 +authors: + - "agent:architect" +supersedes: [] +superseded_by: [] +labels: [] +--- + +# ADR-004 — Спутниковая подложка: провайдер, размещение кода, схема восстановления + +## Статус + +Accepted + +## Контекст + +ET-007 вводит вторую базовую подложку карты — спутниковые растровые +снимки — с переключателем «Схема / Спутник» в попапе слоёв +(см. `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`). + +Существующее состояние, проверенное в коде: + +- В обоих стилях карты (`src/web/style.json` стр. 16–41, + `src/web/style-dark.json`) уже определён единственный raster-source + `osm-raster` и слой `osm-base`, лежащий поверх слоя `background`. + Тайлы OSM раздаются `https://tile.openstreetmap.org/{z}/{x}/{y}.png` + — то есть прецедент **внешней (кросс-оригинальной) тайл-зависимости с + атрибуцией без API-ключа уже существует**. +- `src/web/app.js` (3 132 строки) содержит функцию `rebuildMapOverlays()` + (стр. 127), которая последовательно вызывает `restoreTerrainState()`, + `restoreTrailsState()`, `restorePoiState()`, перерисовку маршрутов / + GPX / линейки. Эта функция — единственная точка восстановления + визуальных слоёв после `map.setStyle()` (переключение тёмной/светлой + темы, `switchMapStyle()` стр. 100–117). +- Фронтенд плоский, без сборщика: `index.html`, `app.js`, `units.js` + (190 строк, ADR-0001), `gpx.js` (1 242 строки, ADR-002). Сложившийся + паттерн — «одна крупная фича = один классический скрипт + глобали» + (ADR-002). Все JS-функции глобальные, обработчики навешаны через + инлайновые `onclick`. +- Динамические мутации слоёв через `setPaintProperty` / + `setLayoutProperty` / `addSource` / `addLayer` в `app.js` уже широко + используются (~30 вхождений). +- В `app.js` уже есть зрелые «restore*State()»-функции для каждой + группы слоёв; ET-007 встраивается в этот же паттерн ещё одной такой + функцией `restoreBaseLayerState()`. + +Решения, которые предстоит зафиксировать архитектурно: + +1. Какого провайдера спутниковых тайлов выбрать. +2. Где разместить код переключателя — в `app.js` или в новом модуле. +3. Как именно добавлять спутниковый source/layer (заранее в `style.json` + или лениво из JS), и как переживать `map.setStyle()`. +4. Каким способом обеспечивать читаемость линий грунтовок/троп и POI + на тёмной спутниковой подложке (halo). +5. Классификацию изменения и нужна ли эскалация `arch:major-change`. + +## Рассмотренные варианты + +### Вариант P (провайдер) — выбор провайдера спутниковых тайлов + +| Провайдер | API-ключ | Лицензия / условия | Покрытие | Решение | +|---|---|---|---|---| +| **Esri World Imagery** (`server.arcgisonline.com/.../World_Imagery/MapServer/tile/{z}/{y}/{x}`) | нет | Условия Esri ArcGIS Online: бесплатное использование с атрибуцией для некоммерческой и demo-разработки; широко применяется open-source-проектами (Leaflet, OpenLayers, QGIS) | глобальное, до z19 | **выбран** | +| Mapbox Satellite | требуется | бесплатный квот-лимит, далее платно | глобальное | отклонён — BRD F-02 явно требует «без API-ключа» | +| Bing Maps | требуется | сложная лицензия, обязательная регистрация | глобальное | отклонён — то же | +| Google Maps Tiles | требуется | прямо запрещён ToS для нативного встраивания не через Google Maps JS API | глобальное | отклонён | +| OpenAerialMap | нет | open-source, CC-BY | **фрагментарное**, нет глобального бесшовного слоя | отклонён — не покрывает РФ-эндуро-сценарии | +| MapTiler Satellite | требуется | бесплатный квот-лимит | глобальное | отклонён — API-ключ | + +Esri World Imagery — единственный вариант, удовлетворяющий +**одновременно** трём ограничениям BRD: без API-ключа, с глобальным +покрытием, с лицензионно допустимой формой использования через +атрибуцию. + +### Вариант M (модуль) — где разместить код + +- **M-A — добавить в `app.js`** (выбран). +~150 строк + (`onBaseLayerToggle`, `applyBaseLayer`, `restoreBaseLayerState`, + `syncBaseLayerUI`, плюс хук в `rebuildMapOverlays()` и handler + `onclick` в `index.html`). Минимальный blast radius, никаких новых + файлов, никаких изменений в подключении скриптов. +- **M-B — выделить `src/web/basemap.js`** (по аналогии с ADR-002 для + GPX). Отклонён: ADR-002 разделил фичу, потому что её объём был + 600–900 строк и она имела собственную модель данных (`gpxTracks`), + собственный bottom sheet и собственный canvas. Здесь фича плоская и + объём в 5–7 раз меньше; разделение даёт чистоту, но не покрывает + стоимости новой связки `app.js ↔ basemap.js` ради ~150 строк. + Контракт интеграции с `rebuildMapOverlays()` и так глобальный — + никакой инкапсуляции отдельный файл не добавит. + +### Вариант S (source) — как добавить спутниковый source/layer + +- **S-A — задекларировать source `satellite-raster` и слой + `satellite-base` (`visibility: none`) в обоих `style.json` / + `style-dark.json`**. Source активен всегда, тайлы не запрашиваются + до показа слоя. Плюс: восстановление после `setStyle()` + тривиально (`setLayoutProperty('satellite-base', 'visibility', ...)`). + Минус: `style.json` обоих тем нужно править симметрично; дрейф + значений между двумя стилями. +- **S-B — лениво создавать source и layer из JS при первом включении + «Спутник»** (выбран, совпадает с TRZ §1 REQ-F-02). Плюс: `style.json` + не трогаем; ноль внешних запросов у пользователей, которые не + включают спутник; единая точка определения source — в `app.js`. После + `map.setStyle()` source и layer исчезают и переcоздаются вызовом + `restoreBaseLayerState()` из `rebuildMapOverlays()` — это та же + логика, что уже используется для terrain/trails/POI/GPX. Минус: + холодное переключение «Схема → Спутник» включает в себя `addSource` + + `addLayer` + сетевой запрос — но укладывается в НФТ 500 мс. + +### Вариант O (order) — порядок восстановления в `rebuildMapOverlays()` + +- **O-A — `restoreBaseLayerState()` вызывается ПЕРВЫМ**, до + `restoreTerrainState()` (выбран, совпадает с TRZ §5.5). Гарантирует + z-order: `background` → `satellite-base` → `osm-base` → terrain → + trails → POI → routes → GPX. terrain/trails/POI оказываются выше + спутника, маршрут/GPX — выше terrain. +- **O-B — добавлять `satellite-base` с явным `beforeId` первого + trails-слоя**. Идемпотентно к порядку, но в `rebuildMapOverlays()` + моменты создания слоёв не атомарны (terrain/trails добавляются + асинхронно); использовать `beforeId` слоёв, которых ещё нет, нельзя. + Поэтому простой «вызвать первым» надёжнее. + +### Вариант H (halo) — обеспечение читаемости поверх спутника + +- **H-A — динамический `setPaintProperty` по всем затрагиваемым слоям**. + Все правки делаем из `applyBaseLayer()`; на «Схема» возвращаем + исходные значения. Минус: нужно где-то хранить «исходные» paint- + значения; при `map.setStyle()` они сбрасываются, что повышает риск + drift между двумя темами. +- **H-B — отдельные «underlay»-слои с halo, `visibility: none` по + умолчанию, включаются на спутнике** + **`setPaintProperty` только + для POI text-halo** (выбран, совпадает с TRZ §1 REQ-F-04). Halo-линии + декларативны в `style.json` обеих тем — никакого «запомнить + исходное» не нужно, восстановление по `visibility`. Для POI label + правок одна (`text-halo-color`/`text-halo-width`) — её проще менять + динамически, чем заводить параллельные label-слои. +- **H-C — толстая полупрозрачная белая обводка прямо в существующих + trails-слоях через `line-gap-width`**. Отклонён: ломает «Схему» + (там halo не нужен и портит вид светлой подложки). + +## Решение + +Принимается комбинация: **P-Esri + M-A + S-B + O-A + H-B**. + +1. **Провайдер — Esri World Imagery.** URL-шаблон, атрибуция и параметры + source — как в TRZ §4.1. HTTPS обязателен. Атрибуция строки — + `"Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community"`. + Альтернативные провайдеры не закладываются в код фичи; точка + расширения — единственный объект source-spec в `applyBaseLayer()`, + при будущей смене провайдера правка локализуется одним местом + (см. R-2 в `10-tech-risks.md`). + +2. **Код фичи живёт в `app.js`.** Никакой новый JS-файл не вводится. + Новые глобальные функции — `onBaseLayerToggle()`, `applyBaseLayer()`, + `restoreBaseLayerState()`, `syncBaseLayerUI()` — добавляются по + соседству с уже существующими `restoreTerrainState()` / + `restoreTrailsState()`. Если в будущей фазе появится потребность + (например, второй провайдер, гибридный режим, оффлайн-кэш) — фича + мигрирует в `src/web/basemap.js` без изменения публичного контракта + (имена функций глобальные и стабильные). + +3. **Source и layer добавляются лениво** при первом включении + «Спутник» через `addSource('satellite-raster', {...})` + + `addLayer({ id: 'satellite-base', ... })`. До этого момента + запросов к `server.arcgisonline.com` не происходит. Это важно с + точки зрения приватности: пользователи, которые никогда не + используют спутник, не светят свой IP на серверы Esri (см. + `10-tech-risks.md`, R-3). + +4. **Восстановление после `map.setStyle()` — через `rebuildMapOverlays()`.** + В функцию добавляется **первым** вызов + `if (typeof restoreBaseLayerState === 'function') restoreBaseLayerState();` + до `restoreTerrainState()`. Это гарантирует, что terrain и trails + окажутся выше спутника, без необходимости вычислять `beforeId`. + `restoreBaseLayerState()` идемпотентен: читает `localStorage` ключа + `map-base-layer` и применяет `applyBaseLayer()`. + +5. **Halo — гибридный подход:** + + - Для **линий grade1..5 и paths/bridleway** в обоих `style.json` / + `style-dark.json` заводятся парные «underlay»-слои + (`*-halo-satellite`) с более широкой полупрозрачной белой + обводкой и `layout.visibility = "none"`. При входе в «Спутник» + эти слои становятся видимыми; при возврате на «Схему» — + скрываются. Никаких runtime-правок paint не требуется. + - Для **POI labels** меняются динамически только два свойства — + `text-halo-color` (`#000000` на спутнике / исходное на схеме) и + `text-halo-width` (`2` на спутнике / исходное на схеме) — через + `setPaintProperty`. Эти исходные значения известны и + зафиксированы в `style.json`; читать «текущее» через + `getPaintProperty` не нужно — всегда выставляем явные значения + для обоих режимов. + - **POI circles** — обводка `circle-stroke-color: #ffffff` / + `circle-stroke-width: 2` динамически на спутнике, возврат к + исходным значениям из `style.json` на схеме. + +6. **Цвет `background`** в режиме «Спутник» меняется через + `setPaintProperty('background', 'background-color', '#2a2a2a')` + (тёмно-серый), чтобы не «бликовало» под медленно подгружающимися + спутниковыми плитками. При возврате на «Схему» восстанавливаются + исходные значения из `style.json` (`#f0ede6` для светлой темы, + тёмное значение из `style-dark.json` для тёмной). Эти константы — + единственные «дублирующие» значения; они зафиксированы в + `applyBaseLayer()` и в `08-data-requirements.md` §5. + +7. **localStorage — ключ `map-base-layer`** (см. TRZ §4.3), значения + `"schematic"` / `"satellite"`, default `"schematic"`. Ключ + полностью обособлен от существующих UI-настроек + (`enduro-theme-mode`, `distance_unit`, `terrain-*`, `trails-*`, + `poi-visible`) — никаких миграций старых значений не требуется. + +8. **C4 / архитектурная диаграмма.** В репозитории нет файлов + `c4-*.mmd`; описание архитектуры — текстовое в + `docs/architecture/README.md`. Туда добавляется отдельный раздел + «Внешние тайл-провайдеры» с двумя строками: OSM (существующий) + и Esri World Imagery (новый, для подложки «Спутник»). Дополнительно + `docs/architecture/adr/README.md` пополняется записью ADR-004. + +## Последствия + +### Положительные + +- Изменения — **только в коде фронтенда** (`src/web/index.html`, + `src/web/app.js`, `src/web/app.css`, оба `style*.json`). Backend, + БД, OSRM, nginx, Docker-конфигурация — без изменений (см. + `07-infra-requirements.md`). +- Лазерная локальность точки расширения: для смены провайдера + достаточно отредактировать один объект source-spec в `app.js`. +- НФТ 500 мс выполнима: при холодном переключении расходы — это + единичные вызовы `addSource` + `addLayer` + первая сетевая загрузка + плитки z=текущий; последующие переключения мгновенные (только + `visibility`). +- Пользователи, никогда не использующие «Спутник», не отправляют ни + одного запроса на серверы Esri — минимизация утечки данных по + умолчанию (см. R-3). +- Существующая инфраструктура восстановления после `map.setStyle()` + переиспользуется без изменения её формы — единый паттерн для + terrain/trails/POI/GPX/base-layer. + +### Отрицательные / ограничения + +- **Зависимость от третьей стороны.** Сервис Esri может ввести + лимит / потребовать API-ключ / изменить URL. Митигация: точка + расширения в `applyBaseLayer()`; риск зафиксирован + (`10-tech-risks.md`, R-2). +- **Утечка IP при использовании спутника.** При активном «Спутник» + IP пользователя становится виден Esri (так же, как сейчас он виден + tile.openstreetmap.org). Это **не регрессия приватности относительно + OSM**, но — расширение перечня третьих сторон, к которым клиент + обращается. Зафиксировано в `08-data-requirements.md` §5 и + `10-tech-risks.md` R-3. +- **Корпоративные / анти-трекинг блокировки.** Часть пользователей + (корпсети, NextDNS-фильтры) могут блокировать `arcgisonline.com`. + Поведение в этом случае — MapLibre показывает прозрачные плитки + поверх `#2a2a2a` фона; пользователь сам переключится на «Схему». + Это нормальное поведение (TRZ §1 REQ-F-08); fallback на схему + автоматически — **не закладываем**. +- **Halo-слои в `style.json` обоих тем.** Любые будущие правки + trails-слоёв требуют согласованной правки соответствующих + `*-halo-satellite` слоёв. Зафиксировано в `10-tech-risks.md` R-1. +- **Background цвет.** В коде `applyBaseLayer()` появляется маленький + дубль констант фона по темам. При смене палитры тем — править здесь + тоже. Зафиксировано в `10-tech-risks.md` R-5. + +### Технический долг + +- Если позже появится потребность во **втором** провайдере (например, + для альтернативной геополитической юрисдикции) или в гибридном + режиме «Спутник + подписи дорог OSM поверх», логичный путь — + вынести фичу в `src/web/basemap.js` (ADR-002-стиль) и расширить + локальное состояние до `{ provider, hybrid }`. Имена глобальных + функций (`onBaseLayerToggle`, `restoreBaseLayerState`) остаются + стабильным контрактом — `index.html` и `app.js` не меняются. +- Если в проект введут CSP-заголовок (сейчас его нет, см. ET-006 + `07-infra-requirements.md` §4), для спутника потребуется + `img-src 'self' https://*.openstreetmap.org https://server.arcgisonline.com data:;`. + +## Классификация изменения + +**Minor change.** Новых контейнеров, сервисов, БД, серверных API +ET-007 не вводит. Внешний тайл-провайдер — расширение уже +существующего класса зависимостей (OSM-tile), а не новый +архитектурный класс. Лейбл `arch:major-change` **не требуется**. +Обязательного дополнительного архитектурного approve не требуется. + +## Связанные документы + +- `docs/work-items/ET-007/01-brd.md` +- `docs/work-items/ET-007/02-trz.md` +- `docs/work-items/ET-007/03-acceptance-criteria.md` +- `docs/work-items/ET-007/04-test-plan.yaml` +- `docs/work-items/ET-007/04b-ui-test-cases.md` +- `docs/work-items/ET-007/07-infra-requirements.md` +- `docs/work-items/ET-007/08-data-requirements.md` +- `docs/work-items/ET-007/10-tech-risks.md` +- `docs/architecture/README.md` +- `docs/architecture/adr/README.md` +- ADR-0001 (ET-005) — паттерн классических скриптов +- ADR-002 (ET-006) — «одна фича = один скрипт + глобали» diff --git a/docs/work-items/ET-007/07-infra-requirements.md b/docs/work-items/ET-007/07-infra-requirements.md new file mode 100644 index 0000000..352b061 --- /dev/null +++ b/docs/work-items/ET-007/07-infra-requirements.md @@ -0,0 +1,163 @@ +--- +type: infra-requirements +work_item_id: ET-007 +title: "Инфраструктурные требования — ET-007: Спутниковая карта (Схема / Спутник)" +version: 1 +status: approved +created_at: 2026-05-31 +authors: + - "agent:architect" +--- + +# Инфраструктурные требования — ET-007 + +## 1. Резюме + +ET-007 — изменение **исключительно фронтенда**: `src/web/index.html`, +`src/web/app.js`, `src/web/app.css`, `src/web/style.json`, +`src/web/style-dark.json`. Новой инфраструктуры, новых контейнеров, +новых портов и серверной конфигурации **не требуется**. Документ +зафиксирован для полноты work-item и явно подтверждает отсутствие +инфра-воздействия (см. `06-adr/ADR-004-satellite-base-layer.md`). + +## 2. Контейнеры и сервисы + +| Аспект | Требование | +|--------|------------| +| Новые контейнеры | Нет | +| Изменения существующих сервисов (api, osrm, nginx) | Нет | +| Изменения `docker-compose.yml` | Нет | +| Изменения `Dockerfile` | Нет — все правки попадают в образ через уже существующий `COPY src/web/ ./src/web/` | +| Изменения подключения скриптов в `index.html` | Нет новых `