Files
enduro-trails/docs/work-items/ET-007/02-trz.md
claude-bot 1984b0bde6
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 6s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 5s
CI / build (push) Successful in 4s
CI / build (pull_request) Successful in 2s
fix(ET-007): address 6 P1 findings from review (docs + code)
12-review.md (REQUEST_CHANGES, attempt 2/3) flagged 6 must-fix items
in the analysis/architecture artefacts plus matching bugs that had
already leaked into the committed implementation. This patch lands
both: documents corrected, code aligned with corrected specs, tests
updated.

P1-1: TRZ/ADR/Data/Risks referenced fictional layer ids
(`trails-grade1..5-halo-satellite`, `paths-bridleway-halo-satellite`).
Actual style*.json has only `trails-track-halo-satellite` and
`trails-path-bridleway-halo-satellite`; grade differentiation lives
inside one `match` expression on `tracktype` within `trails-track`.
Docs rewritten to operate on real ids.

P1-2: POI labels contrast was broken — spec changed only halo-color
to black, leaving `text-color: #333333` (light theme baseline)
unreadable over the new black halo. Code+docs now switch BOTH
`text-color` (-> `#ffffff` on satellite) AND halo together, with
per-theme baselines (`#333333` light / `#e0e0e0` dark) restored on
return to Schematic.

P1-3: BRD §5 hillshade risk said «hillshade auto-disabled on
satellite», contradicting TRZ/ADR/AC. BRD wording aligned: hillshade
keeps working over satellite; visual check is AC-04.

P1-4: background-color had four divergent sources (`#1a1a1a`,
`#2a2a2a`, `#1a1a2e`, `#f0ede6`), incl. an inverted-theme typo and a
baseline `#1a1a1a` that didn't match the actual `style-dark.json:28`
value `#1a1a2e`. Settled on ADR-004's single-constant model: `#2a2a2a`
on satellite for both themes; on Schematic restore per-theme baselines
`#f0ede6` (light) / `#1a1a2e` (dark). `_applyBackgroundForSatellite`
fixed accordingly.

P1-5: app.js already had `layerState.basemap` and `toggleLayer
('basemap')` (legacy «Базовая карта» switch). Neither TRZ nor ADR
specified the interaction. Added save&restore contract: on entering
Satellite save `layerState.basemap` to `_savedBasemapState` and
force-hide `osm-base`; on returning to Schematic restore osm-base
visibility from the saved value. CSS hook `body.satellite-active
#btn-basemap { display:none }` keeps the user from trying to enable
a hybrid mode (out of scope, BRD §3). TRZ §5.6, ADR-004 §8.

P1-6: `restoreTrailsState()` and `onTrailsCheckbox()` only managed
visibility of `trails-track` / `trails-path-bridleway`, leaving
their halo-underlay siblings as «phantom» halos when the user
unchecked grunты/тропы under Satellite. Introduced
`_applyTrailHaloVisibility(map, base)` reading checkbox state from
DOM; called from `onTrailsCheckbox`, `restoreTrailsState`, and both
branches of `applyBaseLayer`. Rule: halo visible ⇔ (base ===
satellite) AND (checkbox ON). TRZ §5.7, ADR-004 §9.

Docs bumped: BRD v2, TRZ v2, AC v2, Data v2, Risks v2; ADR-004
получает «Ревизии»-секцию (status remains accepted — only editorial
fixes, no decision change).

Tests:
- tests/unit/base_layer.test.js: rewritten 2 background-color
  assertions (#1a1a1a expectation removed), added 6 new tests for
  P1-2 / P1-4 (POI text-color per-theme baselines, single satellite
  bg #2a2a2a, baseline restore on Schematic).
- All 33 JS unit tests + 22 pytest static checks green.
- Full pytest suite: 76 passed (excluding pre-existing
  shapely-import skipped collection in tests/unit/test_health.py).

Refs: ET-007
Review: docs/work-items/ET-007/12-review.md (P1-1..P1-6)
ADR: docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md (rev. 2026-05-31)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:05:49 +00:00

499 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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`.
- Под переключателем — горизонтальная линия-разделитель (`<hr>`),
как уже сделано между секциями попапа.
### 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` (сразу после
`<div class="terrain-popup-title">Эндуро</div>` ИЛИ выше него — по
выбору разработчика; рекомендуется в самом верху для большей
заметности):
```html
<!-- ET-007: переключатель подложки (Схема / Спутник) -->
<div class="terrain-base-row">
<span class="terrain-base-label">Подложка</span>
<div class="seg-control base-seg" id="base-seg">
<button type="button" class="seg-btn active" id="base-btn-schematic"
data-base="schematic" onclick="onBaseLayerToggle('schematic')">Схема</button>
<button type="button" class="seg-btn" id="base-btn-satellite"
data-base="satellite" onclick="onBaseLayerToggle('satellite')">Спутник</button>
</div>
</div>
<hr style="margin:6px 0;border-color:rgba(128,128,128,0.3)">
```
### 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:384391` уже определены:
```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:27832826` существуют `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 как «оставить».