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>
27 KiB
type, work_item_id, title, version, status, created_at, updated_at, changelog, authors
| type | work_item_id | title | version | status | created_at | updated_at | changelog | authors | ||
|---|---|---|---|---|---|---|---|---|---|---|
| trz | ET-007 | ТЗ: Спутниковая карта (Схема / Спутник) | 2 | draft | 2026-05-31 | 2026-05-31 |
|
|
ТЗ — ET-007: Спутниковая карта (Схема / Спутник)
1. Функциональные требования
REQ-F-01: Переключатель «Схема / Спутник»
- В попап-панели слоёв (
#terrain-popup, открывается кнопкой#terrain-toggle) добавляется новая секция в самом верху панели — «Подложка». - Реализация — segmented-control (
.seg-control/.seg-btn) с двумя кнопками:- «Схема» (
data-base="schematic", IDbase-btn-schematic) — активна по умолчанию. - «Спутник» (
data-base="satellite", IDbase-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».
- URL-шаблон:
- Источник добавляется на карту лениво: при первом включении режима «Спутник», а не на старте приложения.
- ID источника:
satellite-raster. - ID слоя:
satellite-base.
REQ-F-03: Поведение в режиме «Спутник»
- При включении «Спутник»:
- Если ещё не добавлен — добавить source
satellite-rasterи layersatellite-baseсразу после слояbackground(т.е. ниже всех остальных слоёв). - Слой
osm-base(существующий) скрывается (visibility: none). - Слой
backgroundостаётся (показывает «дыры» если тайлы ещё не загрузились) — цвет фона на спутнике — единая константа#2a2a2aдля обеих тем (тёмно-серый, чтобы не «бликовал» под медленно подгружающимися спутниковыми плитками; решение зафиксировано в ADR-004 §6). Baselinebackground-colorдля возврата на «Схему»:#f0ede6(light),#1a1a2e(dark) — см. Data §5.
- Если ещё не добавлен — добавить source
- При возврате на «Схема»:
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> ИЛИ выше него — по
выбору разработчика; рекомендуется в самом верху для большей
заметности):
<!-- 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):
.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)
{
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)
{
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) добавить вызов
первым:
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 уже определены:
const layerState = { tracks: true, paths: true, poi: true, basemap: true };
const layerGroups = { …, basemap: ['osm-base'] };
function toggleLayer(group) { …setLayoutProperty('osm-base', 'visibility', …) }
— это существующий механизм «Базовая карта (схема)» как самостоятельного выключателя. ET-007 уважает этот механизм по следующему контракту:
- При входе в «Спутник» (
applyBaseLayer('satellite'), §5.2 шаг 2.4): запомнитьlayerState.basemapв локальной переменной_savedBasemapState(init:null). Затем принудительно скрытьosm-base.layerState.basemapне меняется — UI-кнопка#btn-basemapостаётся в прежнем визуальном состоянии. - Пока активен «Спутник», кнопка «Базовая карта» скрыта из UI
(CSS-класс
.satellite-activeна корне приложения скрывает#btn-basemap) — пользователь не должен пытаться включить схему поверх спутника (гибридный режим out of scope BRD §3). Альтернатива реализации — disabled, на усмотрение разработчика; визуальный эффект и AC-02/AC-03 идентичны. - При возврате на «Схему» (§5.2 шаг 3.1):
osm-base.visibilityвосстанавливается из_savedBasemapState(по умолчаниюtrue→'visible', если ранее пользователь сам выключал —false→'none'). После восстановления_savedBasemapState = null. - На «Схеме» (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).
Реализация:
- Ввести хелпер
applyTrailHaloVisibility(trackOn, pathOn):- для пары
('trails-track-halo-satellite', trackOn)и('trails-path-bridleway-halo-satellite', pathOn):visibility = (currentBaseLayer === 'satellite' && checked) ? 'visible' : 'none'.
- для пары
- В
onTrailsCheckbox()после установкиvisibilityосновным слоям — вызватьapplyTrailHaloVisibility(trackChecked, pathChecked). - В
restoreTrailsState()после установкиvisibilityосновным слоям — вызватьapplyTrailHaloVisibility(trackOn, pathOn). - В
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 как «оставить».