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/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 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..aea4b29 --- /dev/null +++ b/docs/work-items/ET-007/01-brd.md @@ -0,0 +1,100 @@ +--- +type: brd +work_item_id: ET-007 +title: "BRD: Спутниковая карта (Схема / Спутник)" +version: 2 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +changelog: + - "v2 (2026-05-31): code-review fix (12-review.md P1-3) — митигация риска hillshade приведена в соответствие с TRZ/ADR/AC: авто-выключение не вводится." +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 §1 REQ-F-04, ADR-004 §«Контекст 1.5»); визуальная проверка — UI-тест AC-04 «Hillshade поверх спутника» | +| Юридические ограничения на использование стороннего провайдера спутниковых тайлов | Низкая | Высокое | В 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..63d7125 --- /dev/null +++ b/docs/work-items/ET-007/02-trz.md @@ -0,0 +1,498 @@ +--- +type: trz +work_item_id: ET-007 +title: "ТЗ: Спутниковая карта (Схема / Спутник)" +version: 2 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +changelog: + - "v2 (2026-05-31): code-review fixes (12-review.md, attempt 2/3) — P1-1..P1-6: реальные id halo-слоёв, контраст POI labels, единый satellite-bg, контракт с layerState.basemap, синхронизация halo с чекбоксами." +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` + для обеих тем (тёмно-серый, чтобы не «бликовал» под медленно + подгружающимися спутниковыми плитками; решение зафиксировано в + ADR-004 §6). Baseline `background-color` для возврата на «Схему»: + `#f0ede6` (light), `#1a1a2e` (dark) — см. Data §5. +- При возврате на «Схема»: + - `osm-base` снова видим (`visibility: visible`). + - `satellite-base` скрывается (`visibility: none`), но не удаляется + из стиля (быстрое повторное переключение). + +### REQ-F-04: Совместимость со слоями приложения + +Все клиентские слои должны корректно отображаться поверх спутника: + +| Слой | Z-order над спутником | Доп. правила в режиме «Спутник» | +| ----------------------------- | --------------------- | ------------------------------------------------------------------------------ | +| Hillshade (`terrain-hillshade`) | поверх спутника | Включается/выключается чекбоксом как раньше; по умолчанию НЕ авто-выключается | +| TRI (`terrain-tri`) | поверх спутника | Аналогично hillshade | +| Trails — грунтовки (`trails-track`) | поверх terrain | Halo через парный underlay-слой `trails-track-halo-satellite` (единый halo на весь слой, без разбиения по grade) | +| Paths / bridleway (`trails-path-bridleway`) | поверх trails | Halo через парный underlay-слой `trails-path-bridleway-halo-satellite` | +| Asphalt-дороги (`trails-asphalt`) | поверх trails | Halo не вводится — слой по умолчанию скрыт (`visibility: none`, `line-opacity: 0`); если будет включён в будущем, halo добавляется тем же паттерном | +| POI circles (`poi-circles`) | поверх trails | Обводка `circle-stroke-color: #ffffff`, толщина 2 px | +| POI labels (`poi-labels`) | поверх POI | `text-color: #ffffff`, `text-halo-color: #000000`, `text-halo-width: 2` для читаемости на спутнике (см. REQ-F-04-POI ниже) | +| Route / Scenic / Link / Ruler | поверх POI | Без изменений | +| GPX-треки и waypoints | поверх Route | Без изменений (ET-006 уже совместим) | + +**REQ-F-04-POI (контраст подписей POI на спутнике).** На спутнике +менять обе пары свойств `text-color` и `text-halo-*`, иначе тёмный +текст `#333333` (light-theme) останется нечитаем поверх тёмного halo. +Конкретные значения и baseline-возврат — в Data §5. + +**Halo-слои в `style*.json` (подтверждено фактическим кодом +`src/web/style.json` и `style-dark.json`):** реальные id — это +`trails-track-halo-satellite` и `trails-path-bridleway-halo-satellite`. +Слоёв `trails-grade1..5-halo-satellite` или +`paths-bridleway-halo-satellite` **нет** и заводить их не нужно: +`trails-track` хранит дифференциацию по grade внутри одного `match`- +выражения по `tracktype`. На спутнике halo единого цвета/ширины +накладывается на весь `trails-track` целиком; разделять halo по grade +не требуется (визуально не различимо под линией grade-цвета). + +Реализация: +- Halo для грунтовок и троп — пара underlay-слоёв + (`trails-track-halo-satellite`, `trails-path-bridleway-halo-satellite`), + уже присутствующих в обоих `style*.json` с `visibility: none`. + Включаются через `setLayoutProperty(..., 'visibility', 'visible')` + только в режиме «Спутник». +- Стили POI (circles и labels) на спутнике задаются динамически через + `setPaintProperty` при переключении режима; baseline-значения + возврата на «Схему» зафиксированы в `08-data-requirements.md` §5 + и в `applyBaseLayer()` (см. §5.2 ниже). + +### 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. Корректный z-order гарантируется тем, что + restoreBaseLayerState вызывается ПЕРВЫМ в rebuildMapOverlays + (см. ADR-004 §«Вариант O», O-A; см. также R-7 в Tech-Risks). + 2.3. setLayoutProperty('satellite-base', 'visibility', 'visible'). + 2.4. Запомнить layerState.basemap в _savedBasemapState (см. §5.6). + Принудительно скрыть osm-base: + setLayoutProperty('osm-base', 'visibility', 'none'). + 2.5. Включить halo-слои (см. §5.7 — синхронизация с чекбоксами): + для каждой пары (base, halo) ∈ + [('trails-track', 'trails-track-halo-satellite'), + ('trails-path-bridleway', 'trails-path-bridleway-halo-satellite')] + выставить halo.visibility = base.visibility текущего слоя. + 2.6. Применить динамические правки POI: + - poi-circles: circle-stroke-color = '#ffffff', + circle-stroke-width = 2; + - poi-labels: text-color = '#ffffff', + text-halo-color = '#000000', + text-halo-width = 2. + 2.7. Сменить background-color на единую satellite-константу + '#2a2a2a' (для обеих тем, см. ADR-004 §6). +3. Иначе (base === 'schematic'): + 3.1. setLayoutProperty('osm-base', 'visibility', + _savedBasemapState === false ? 'none' : 'visible') — + восстановить выбор пользователя по «Базовая карта» + (см. §5.6); по умолчанию (если не сохранено) — 'visible'. + 3.2. setLayoutProperty('satellite-base', 'visibility', 'none') + (если слой существует). + 3.3. Скрыть halo-underlay-слои: + для обеих пар выставить halo.visibility = 'none'. + 3.4. Вернуть POI к baseline текущей темы (см. Data §5): + - poi-circles: circle-stroke-color / circle-stroke-width + читаются из Data §5 baseline (поэтапно: light → dark); + - poi-labels: text-color, text-halo-color, text-halo-width — то же. + Источник истины baseline'ов — Data §5; код держит две константы + per-theme и выбирает по текущей теме. + 3.5. Background-color — установить baseline текущей темы из Data §5 + ('#f0ede6' light / '#1a1a2e' dark). Прямая запись через + setPaintProperty (не полагаемся на setStyle, потому что + applyBaseLayer вызывается и без смены стиля). +``` + +### 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/ADR-004 O-A: восстановить подложку ПЕРВОЙ — terrain/trails/POI + // ложатся поверх неё (z-order через порядок вставки, без beforeId). + // Функция определена в этом же файле (ADR-004 §2), глобально доступна. + restoreBaseLayerState(); + // ── далее без изменений ── + restoreTerrainState(); + restoreTrailsState(); + // ... +} +``` + +### 5.6 Взаимодействие с существующим `toggleLayer('basemap')` + +В `app.js:384–391` уже определены: + +```js +const layerState = { tracks: true, paths: true, poi: true, basemap: true }; +const layerGroups = { …, basemap: ['osm-base'] }; +function toggleLayer(group) { …setLayoutProperty('osm-base', 'visibility', …) } +``` + +— это существующий механизм «Базовая карта (схема)» как +самостоятельного выключателя. ET-007 уважает этот механизм по +следующему контракту: + +1. **При входе в «Спутник»** (`applyBaseLayer('satellite')`, §5.2 шаг + 2.4): запомнить `layerState.basemap` в локальной переменной + `_savedBasemapState` (init: `null`). Затем **принудительно** скрыть + `osm-base`. `layerState.basemap` **не меняется** — UI-кнопка + `#btn-basemap` остаётся в прежнем визуальном состоянии. +2. **Пока активен «Спутник»**, кнопка «Базовая карта» скрыта из UI + (CSS-класс `.satellite-active` на корне приложения скрывает + `#btn-basemap`) — пользователь не должен пытаться включить схему + поверх спутника (гибридный режим out of scope BRD §3). Альтернатива + реализации — disabled, на усмотрение разработчика; визуальный + эффект и AC-02/AC-03 идентичны. +3. **При возврате на «Схему»** (§5.2 шаг 3.1): `osm-base.visibility` + восстанавливается из `_savedBasemapState` (по умолчанию `true` → + `'visible'`, если ранее пользователь сам выключал — `false` → + `'none'`). После восстановления `_savedBasemapState = null`. +4. **На «Схеме» (default-режим)**: `toggleLayer('basemap')` работает + ровно как раньше — пишет в `layerState.basemap` и переключает + `osm-base.visibility`. ET-007 этот код не трогает. + +### 5.7 Синхронизация halo с чекбоксами «Грунтовки» / «Тропы» / «POI» + +В `app.js:2783–2826` существуют `onTrailsCheckbox()` и +`restoreTrailsState()`, которые управляют `visibility` только +`trails-track` и `trails-path-bridleway`. Halo-underlay-слои +(`*-halo-satellite`) сейчас они не трогают — в режиме «Спутник» это +дало бы «фантом» halo без основной линии. + +Правило (источник истины): **halo-слой видим ⇔ (текущая база === +'satellite') AND (соответствующий пользовательский чекбокс ON)**. + +Реализация: + +1. Ввести хелпер `applyTrailHaloVisibility(trackOn, pathOn)`: + - для пары `('trails-track-halo-satellite', trackOn)` и + `('trails-path-bridleway-halo-satellite', pathOn)`: + `visibility = (currentBaseLayer === 'satellite' && checked) ? 'visible' : 'none'`. +2. В `onTrailsCheckbox()` после установки `visibility` основным слоям — + вызвать `applyTrailHaloVisibility(trackChecked, pathChecked)`. +3. В `restoreTrailsState()` после установки `visibility` основным слоям — + вызвать `applyTrailHaloVisibility(trackOn, pathOn)`. +4. В `applyBaseLayer('satellite')` (§5.2 шаг 2.5) и + `applyBaseLayer('schematic')` (§5.2 шаг 3.3) — читать текущее + состояние чекбоксов из DOM (`#trails-track-cb`, `#trails-path-cb`) + и вызвать тот же хелпер. + +**POI:** для группы `poi-circles` / `poi-labels` отдельных +halo-underlay-слоёв нет — динамические правки `setPaintProperty` +(см. §5.2) уже привязаны к видимости самих слоёв. При выключении +чекбокса «POI» оба слоя становятся `visibility: none` через +существующий механизм `layerState.poi`/`restorePoiState()` — текстовые +halo-свойства просто не видны, поэтому отдельная синхронизация не +требуется. + +## 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..f156df8 --- /dev/null +++ b/docs/work-items/ET-007/03-acceptance-criteria.md @@ -0,0 +1,263 @@ +--- +type: acceptance-criteria +work_item_id: ET-007 +title: "AC: Спутниковая карта (Схема / Спутник)" +version: 2 +status: draft +created_at: 2026-05-31 +updated_at: 2026-05-31 +changelog: + - "v2 (2026-05-31): code-review fixes (12-review.md P1-2, P1-5, P1-6) — добавлены сценарии: видимость #btn-basemap при входе/выходе из «Спутник», save&restore _savedBasemapState, синхронизация halo с чекбоксами Грунтовки/Тропы, явные значения POI text-color/halo на спутнике и baseline при возврате." +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» + + Scenario: Кнопка «Базовая карта» скрывается на спутнике (P1-5) + Given активна подложка «Спутник» + Then UI-кнопка #btn-basemap не видна пользователю + And пользователь не может из UI включить osm-base поверх спутника (out of scope, BRD §3 — гибридный режим) + + Scenario: Запоминание выбора «Базовая карта» при входе в Спутник (P1-5) + Given активна подложка «Схема» + And пользователь явно выключил «Базовую карту» (layerState.basemap === false, osm-base.visibility === 'none') + When пользователь переключается на «Спутник» + Then значение layerState.basemap сохраняется во внутреннем _savedBasemapState === false + And osm-base.visibility остаётся 'none' (принудительно) +``` + +## AC-03: Переключение на «Схема» + +```gherkin +Feature: Переключение Спутник → Схема + + Scenario: Возврат на схему (layerState.basemap по умолчанию true) + Given активна подложка «Спутник» + And до входа в «Спутник» layerState.basemap === true (default) + When пользователь нажимает «Схема» в попапе слоёв + Then кнопка «Схема» получает класс .active + And слой osm-base снова виден (visibility=visible) + And слой satellite-base скрыт (visibility=none), но source остаётся в стиле + And положение карты не изменилось + And UI-кнопка #btn-basemap снова видна + + Scenario: Возврат на схему с восстановлением выбора пользователя (P1-5) + Given активна подложка «Спутник» + And до входа в «Спутник» пользователь выключил «Базовую карту» (_savedBasemapState === false) + When пользователь нажимает «Схема» + Then слой osm-base остаётся скрытым (visibility=none) — выбор пользователя восстановлен + And layerState.basemap === false + And _savedBasemapState сбрасывается в null +``` + +## AC-04: Совместимость со слоями приложения + +```gherkin +Feature: Слои поверх спутника + + Scenario: Грунтовки и тропы видны на спутнике + Given активна подложка «Спутник» + And в попапе включены «Грунтовки» и «Тропы» + Then на карте видны линии грунтовок (trails-track) и троп (trails-path-bridleway) поверх спутника + And halo-слой trails-track-halo-satellite visibility=visible + And halo-слой trails-path-bridleway-halo-satellite visibility=visible + + Scenario: Выключение «Грунтовки» скрывает и halo (P1-6) + Given активна подложка «Спутник» + And чекбокс «Грунтовки» был ON + When пользователь снимает чекбокс «Грунтовки» + Then trails-track visibility=none + And trails-track-halo-satellite visibility=none (halo не остаётся «фантомом») + + Scenario: Выключение «Тропы» скрывает и halo (P1-6) + Given активна подложка «Спутник» + And чекбокс «Тропы» был ON + When пользователь снимает чекбокс «Тропы» + Then trails-path-bridleway visibility=none + And trails-path-bridleway-halo-satellite visibility=none + + Scenario: На «Схеме» halo-слои всегда скрыты (P1-6) + Given активна подложка «Схема» + And чекбокс «Грунтовки» ON + Then trails-track visibility=visible + And trails-track-halo-satellite visibility=none + + Scenario: POI видны и читаемы на спутнике (P1-2) + Given активна подложка «Спутник» + And в попапе включён «POI» + Then на карте видны маркеры POI поверх спутника + And poi-labels paint: text-color === '#ffffff' + And poi-labels paint: text-halo-color === '#000000' + And poi-labels paint: text-halo-width === 2 + And poi-circles paint: circle-stroke-color === '#ffffff' + And poi-circles paint: circle-stroke-width === 2 + + Scenario: POI baseline восстанавливается на «Схеме» (P1-2) + Given был активен «Спутник», POI labels в режиме спутника + When пользователь возвращается на «Схему» (light-тема) + Then poi-labels paint: text-color === '#333333' (baseline light, Data §5) + And poi-labels paint: text-halo-color === '#ffffff' (baseline light) + And poi-labels paint: text-halo-width === 1.5 (baseline light) + + 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: "Подложка остаётся «Спутник», единицы переключены на мили (кнопка «мили» подсвечена). Оба переключателя видны и работают независимо" 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..e40afad --- /dev/null +++ b/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md @@ -0,0 +1,370 @@ +--- +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 — гибридный подход:** + + - Для **линий грунтовок и троп** в обоих `style.json` / + `style-dark.json` присутствуют парные «underlay»-слои + `trails-track-halo-satellite` и + `trails-path-bridleway-halo-satellite` (более широкая + полупрозрачная белая обводка, `layout.visibility = "none"`). + При входе в «Спутник» эти слои становятся видимыми; при возврате + на «Схему» — скрываются. Никаких runtime-правок paint не + требуется. Слоёв на каждую grade (`trails-grade1..5-halo-satellite`) + **не заводится**: дифференциация grade хранится внутри одного + `match`-выражения по `tracktype` в `trails-track`, halo единого + цвета/ширины накладывается на весь слой целиком — этого + достаточно для читаемости (под halo всё равно ляжет цветная + линия `trails-track`). Аналогично для троп — единый halo на весь + `trails-path-bridleway` (фильтр `highway in path/bridleway/footway`). + `trails-asphalt` halo не получает: он по умолчанию скрыт + (`visibility: none`, `line-opacity: 0`); если в будущей фазе + включится — добавится halo тем же паттерном. + - Для **POI labels** меняются динамически три свойства: + `text-color` (`#ffffff` на спутнике / baseline текущей темы на схеме — + `#333333` для light, `#e0e0e0` для dark), `text-halo-color` + (`#000000` на спутнике / baseline `#ffffff` для light, + `#1a1a2e` для dark на схеме), `text-halo-width` (`2` на спутнике + / baseline `1.5` для light, `2` для dark на схеме). Менять + **обе** пары (color + halo) необходимо: иначе тёмный baseline- + текст светлой темы (`#333333`) поверх чёрного halo не читается. + Baseline-значения известны и зафиксированы в Data §5; всегда + выставляем явные значения для обоих режимов. + - **POI circles** — обводка `circle-stroke-color: #ffffff` / + `circle-stroke-width: 2` динамически на спутнике, возврат к + baseline текущей темы из Data §5 на схеме (`#ffffff`/`1.5` light, + `#333333`/`1.5` dark). + +6. **Цвет `background`** в режиме «Спутник» меняется через + `setPaintProperty('background', 'background-color', '#2a2a2a')` — + **единая константа `#2a2a2a` для обеих тем** (тёмно-серый, чтобы + не «бликовал» под медленно подгружающимися спутниковыми плитками). + На обеих темах используется одно и то же значение; per-theme- + развилки нет (упрощает код и исключает рассинхрон). При возврате + на «Схему» восстанавливаются baseline-значения текущей темы — + `#f0ede6` (light, из `style.json`) и `#1a1a2e` (dark, из + `style-dark.json`; **не** `#1a1a1a` — это была ошибка в более + раннем черновике). Эти baseline-константы зафиксированы в + `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. **Контракт с существующим `toggleLayer('basemap')` + (`app.js:384–391`).** В коде уже есть отдельный пользовательский + выключатель «Базовая карта» (управляет `osm-base.visibility` и + `layerState.basemap`). ET-007 принимает паттерн **save & restore** + (см. TRZ §5.6): при входе в «Спутник» сохраняем `layerState.basemap` + в `_savedBasemapState` и принудительно скрываем `osm-base`; UI-кнопка + `#btn-basemap` скрывается через CSS-класс `.satellite-active` (чтобы + пользователь не пытался включить «гибрид»: out of scope BRD §3). + При возврате на «Схему» восстанавливаем `osm-base.visibility` из + сохранённого значения. На «Схеме» `toggleLayer('basemap')` работает + как раньше — ET-007 этот код не трогает. + +9. **Синхронизация halo-слоёв с пользовательскими чекбоксами + «Грунтовки» / «Тропы» (`app.js:2783–2826`).** В существующих + `onTrailsCheckbox()` / `restoreTrailsState()` управляется + видимость только `trails-track` и `trails-path-bridleway`. Halo- + underlay-слои сами по себе не отслеживаются; на спутнике это даёт + «фантом» halo при выключенной грунтовке/тропе. Решение (TRZ §5.7): + ввести единый хелпер `applyTrailHaloVisibility(trackOn, pathOn)` + и вызывать его из (а) `onTrailsCheckbox`, (б) `restoreTrailsState`, + (в) `applyBaseLayer('satellite' | 'schematic')`. Правило: halo + видим ⇔ `currentBaseLayer === 'satellite' AND checkbox === ON`. + POI отдельной синхронизации не требуют — paint-правки текста + привязаны к самим `poi-circles`/`poi-labels`, которые управляются + `layerState.poi` / `restorePoiState()`. + +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 не требуется. + +## Ревизии + +- 2026-05-31 — editorial: code-review fixes (12-review.md attempt 2/3). + Решения P/M/S/O/H **не пересматривались**. Правки: + - §5 пункт 1: реальные id halo-слоёв + (`trails-track-halo-satellite`, `trails-path-bridleway-halo-satellite`) + вместо несуществующих `trails-grade1..5-halo-satellite` / + `paths-bridleway-halo-satellite` (P1-1). + - §5 пункт 2 (POI labels): добавлена правка `text-color` на + спутнике + явный baseline возврата per-theme — без этого тёмный + `#333333` поверх чёрного halo был нечитаем (P1-2). + - §6: зафиксирована единая satellite-константа `#2a2a2a` для обеих + тем; baseline dark исправлен `#1a1a1a` → `#1a1a2e` под фактическое + значение `style-dark.json:28` (P1-4). + - Добавлен §8: контракт с существующим `toggleLayer('basemap')` / + `layerState.basemap` — паттерн save&restore через + `_savedBasemapState` (P1-5). + - Добавлен §9: синхронизация halo-слоёв с пользовательскими + чекбоксами «Грунтовки»/«Тропы» — хелпер + `applyTrailHaloVisibility` (P1-6). + +## Связанные документы + +- `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` | Нет новых `