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