Files
enduro-trails/docs/work-items/ET-007/02-trz.md
claude-bot d7d06bb046
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 6s
CI / build (push) Successful in 1s
docs(ET-007): analyst artifacts - BRD, TRZ, AC, TestPlan, UI tests
2026-05-31 18:28:31 +00:00

18 KiB
Raw Blame History

type, work_item_id, title, version, status, created_at, updated_at, authors
type work_item_id title version status created_at updated_at authors
trz ET-007 ТЗ: Спутниковая карта (Схема / Спутник) 1 draft 2026-05-31 2026-05-31
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 для тёмной темы и #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 (сразу после <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 = 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) добавить вызов первым:

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