--- 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 как «оставить».