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

27 KiB
Raw Permalink Blame History

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
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 с чекбоксами.
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> ИЛИ выше него — по выбору разработчика; рекомендуется в самом верху для большей заметности):

<!-- 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:384391 уже определены:

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