fix(ET-007): address 6 P1 findings from review (docs + code)
All checks were successful
All checks were successful
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>
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
type: brd
|
||||
work_item_id: ET-007
|
||||
title: "BRD: Спутниковая карта (Схема / Спутник)"
|
||||
version: 1
|
||||
version: 2
|
||||
status: draft
|
||||
created_at: 2026-05-31
|
||||
updated_at: 2026-05-31
|
||||
changelog:
|
||||
- "v2 (2026-05-31): code-review fix (12-review.md P1-3) — митигация риска hillshade приведена в соответствие с TRZ/ADR/AC: авто-выключение не вводится."
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
@@ -76,7 +78,7 @@ authors:
|
||||
| Провайдер спутниковых тайлов закроет доступ / введёт лимит / потребует API-ключ | Средняя | Высокое | Зафиксировать конкретного провайдера в ADR; предусмотреть точку расширения для альтернативного провайдера (несколько URL) |
|
||||
| Спутниковая подложка медленно грузится → пользователь видит «дыры» | Высокая | Среднее | Использовать background-цвет (тёмно-серый) под спутником; OSM-схема остаётся как fallback в случае ошибки загрузки тайлов |
|
||||
| Цвет грунтовок и троп плохо виден на спутниковой подложке | Высокая | Среднее | TRZ: на режиме «Спутник» включается обводка (halo) у линий грунтовок и троп — по аналогии с подписями POI |
|
||||
| Hillshade поверх спутника даёт некрасивое наложение (двойное затенение рельефа) | Средняя | Низкое | По умолчанию hillshade отключается при включении спутника — поведение фиксируется в TRZ |
|
||||
| Hillshade поверх спутника даёт некрасивое наложение (двойное затенение рельефа) | Средняя | Низкое | Hillshade продолжает работать поверх спутника как и поверх схемы — авто-выключение не вводится (TRZ §1 REQ-F-04, ADR-004 §«Контекст 1.5»); визуальная проверка — UI-тест AC-04 «Hillshade поверх спутника» |
|
||||
| Юридические ограничения на использование стороннего провайдера спутниковых тайлов | Низкая | Высокое | В ADR указать выбранного провайдера с лицензией, разрешающей использование без API-ключа (Esri World Imagery, ArcGIS) |
|
||||
| Регресс UI на мобильных устройствах из-за нового переключателя | Низкая | Среднее | UI-тест-кейсы (04b) для desktop и mobile viewport |
|
||||
| Конфликт с уже сохранёнными localStorage-значениями старых версий | Низкая | Низкое | Использовать новый ключ `map-base-layer`, default = `schematic` |
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
type: trz
|
||||
work_item_id: ET-007
|
||||
title: "ТЗ: Спутниковая карта (Схема / Спутник)"
|
||||
version: 1
|
||||
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"
|
||||
---
|
||||
@@ -50,9 +52,11 @@ authors:
|
||||
остальных слоёв).
|
||||
- Слой `osm-base` (существующий) скрывается (`visibility: none`).
|
||||
- Слой `background` остаётся (показывает «дыры» если тайлы ещё не
|
||||
загрузились) — цвет фона `#2a2a2a` для тёмной темы и `#1a1a1a` для
|
||||
светлой темы в режиме «Спутник» (чтобы белый фон не «бликовал» под
|
||||
тёмными снимками).
|
||||
загрузились) — цвет фона на спутнике — единая константа `#2a2a2a`
|
||||
для обеих тем (тёмно-серый, чтобы не «бликовал» под медленно
|
||||
подгружающимися спутниковыми плитками; решение зафиксировано в
|
||||
ADR-004 §6). Baseline `background-color` для возврата на «Схему»:
|
||||
`#f0ede6` (light), `#1a1a2e` (dark) — см. Data §5.
|
||||
- При возврате на «Схема»:
|
||||
- `osm-base` снова видим (`visibility: visible`).
|
||||
- `satellite-base` скрывается (`visibility: none`), но не удаляется
|
||||
@@ -66,19 +70,39 @@ authors:
|
||||
| ----------------------------- | --------------------- | ------------------------------------------------------------------------------ |
|
||||
| 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` для читаемости на спутнике |
|
||||
| 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»-слои с
|
||||
более широкой полупрозрачной белой линией; включать их через
|
||||
`visibility` только в режиме «Спутник».
|
||||
- Стили POI на спутнике задаются динамически через `setPaintProperty`
|
||||
при переключении режима.
|
||||
- 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)
|
||||
|
||||
@@ -300,23 +324,46 @@ authors:
|
||||
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.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. setLayoutProperty('osm-base', 'visibility', 'none').
|
||||
2.5. Применить «спутниковые» правки к слоям trails/path/poi:
|
||||
- усилить halo у line-слоёв (через setPaintProperty);
|
||||
- сделать POI text-halo чёрным.
|
||||
2.6. Сменить background-color на тёмно-серый (#2a2a2a).
|
||||
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', 'visible').
|
||||
3.1. setLayoutProperty('osm-base', 'visibility',
|
||||
_savedBasemapState === false ? 'none' : 'visible') —
|
||||
восстановить выбор пользователя по «Базовая карта»
|
||||
(см. §5.6); по умолчанию (если не сохранено) — 'visible'.
|
||||
3.2. setLayoutProperty('satellite-base', 'visibility', 'none')
|
||||
(если слой существует).
|
||||
3.3. Вернуть halo trails / POI к дефолтным значениям из текущего стиля.
|
||||
3.4. Background-color — из исходного стиля (не трогать,
|
||||
он восстанавливается при setStyle).
|
||||
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()`
|
||||
@@ -341,11 +388,10 @@ authors:
|
||||
|
||||
```js
|
||||
function rebuildMapOverlays() {
|
||||
// ET-007: восстановить выбранную подложку первой —
|
||||
// чтобы terrain/trails/POI применили свои overlays поверх неё
|
||||
if (typeof restoreBaseLayerState === 'function') {
|
||||
restoreBaseLayerState();
|
||||
}
|
||||
// ET-007/ADR-004 O-A: восстановить подложку ПЕРВОЙ — terrain/trails/POI
|
||||
// ложатся поверх неё (z-order через порядок вставки, без beforeId).
|
||||
// Функция определена в этом же файле (ADR-004 §2), глобально доступна.
|
||||
restoreBaseLayerState();
|
||||
// ── далее без изменений ──
|
||||
restoreTerrainState();
|
||||
restoreTrailsState();
|
||||
@@ -353,6 +399,73 @@ function rebuildMapOverlays() {
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 Взаимодействие с существующим `toggleLayer('basemap')`
|
||||
|
||||
В `app.js:384–391` уже определены:
|
||||
|
||||
```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:2783–2826` существуют `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. Файловая структура изменений
|
||||
|
||||
```
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
type: acceptance-criteria
|
||||
work_item_id: ET-007
|
||||
title: "AC: Спутниковая карта (Схема / Спутник)"
|
||||
version: 1
|
||||
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 P1-2, P1-5, P1-6) — добавлены сценарии: видимость #btn-basemap при входе/выходе из «Спутник», save&restore _savedBasemapState, синхронизация halo с чекбоксами Грунтовки/Тропы, явные значения POI text-color/halo на спутнике и baseline при возврате."
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
@@ -48,6 +50,18 @@ Feature: Переключение Схема → Спутник
|
||||
Scenario: Атрибуция Esri отображается
|
||||
Given пользователь включил режим «Спутник»
|
||||
Then в нижнем правом углу карты видна атрибуция «Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community»
|
||||
|
||||
Scenario: Кнопка «Базовая карта» скрывается на спутнике (P1-5)
|
||||
Given активна подложка «Спутник»
|
||||
Then UI-кнопка #btn-basemap не видна пользователю
|
||||
And пользователь не может из UI включить osm-base поверх спутника (out of scope, BRD §3 — гибридный режим)
|
||||
|
||||
Scenario: Запоминание выбора «Базовая карта» при входе в Спутник (P1-5)
|
||||
Given активна подложка «Схема»
|
||||
And пользователь явно выключил «Базовую карту» (layerState.basemap === false, osm-base.visibility === 'none')
|
||||
When пользователь переключается на «Спутник»
|
||||
Then значение layerState.basemap сохраняется во внутреннем _savedBasemapState === false
|
||||
And osm-base.visibility остаётся 'none' (принудительно)
|
||||
```
|
||||
|
||||
## AC-03: Переключение на «Схема»
|
||||
@@ -55,13 +69,23 @@ Feature: Переключение Схема → Спутник
|
||||
```gherkin
|
||||
Feature: Переключение Спутник → Схема
|
||||
|
||||
Scenario: Возврат на схему
|
||||
Scenario: Возврат на схему (layerState.basemap по умолчанию true)
|
||||
Given активна подложка «Спутник»
|
||||
And до входа в «Спутник» layerState.basemap === true (default)
|
||||
When пользователь нажимает «Схема» в попапе слоёв
|
||||
Then кнопка «Схема» получает класс .active
|
||||
And слой osm-base снова виден (visibility=visible)
|
||||
And слой satellite-base скрыт (visibility=none), но source остаётся в стиле
|
||||
And положение карты не изменилось
|
||||
And UI-кнопка #btn-basemap снова видна
|
||||
|
||||
Scenario: Возврат на схему с восстановлением выбора пользователя (P1-5)
|
||||
Given активна подложка «Спутник»
|
||||
And до входа в «Спутник» пользователь выключил «Базовую карту» (_savedBasemapState === false)
|
||||
When пользователь нажимает «Схема»
|
||||
Then слой osm-base остаётся скрытым (visibility=none) — выбор пользователя восстановлен
|
||||
And layerState.basemap === false
|
||||
And _savedBasemapState сбрасывается в null
|
||||
```
|
||||
|
||||
## AC-04: Совместимость со слоями приложения
|
||||
@@ -72,14 +96,46 @@ Feature: Слои поверх спутника
|
||||
Scenario: Грунтовки и тропы видны на спутнике
|
||||
Given активна подложка «Спутник»
|
||||
And в попапе включены «Грунтовки» и «Тропы»
|
||||
Then на карте видны линии грунтовок и троп поверх спутника
|
||||
And линии имеют визуально различимую обводку (halo) для контраста
|
||||
Then на карте видны линии грунтовок (trails-track) и троп (trails-path-bridleway) поверх спутника
|
||||
And halo-слой trails-track-halo-satellite visibility=visible
|
||||
And halo-слой trails-path-bridleway-halo-satellite visibility=visible
|
||||
|
||||
Scenario: POI видны на спутнике
|
||||
Scenario: Выключение «Грунтовки» скрывает и halo (P1-6)
|
||||
Given активна подложка «Спутник»
|
||||
And чекбокс «Грунтовки» был ON
|
||||
When пользователь снимает чекбокс «Грунтовки»
|
||||
Then trails-track visibility=none
|
||||
And trails-track-halo-satellite visibility=none (halo не остаётся «фантомом»)
|
||||
|
||||
Scenario: Выключение «Тропы» скрывает и halo (P1-6)
|
||||
Given активна подложка «Спутник»
|
||||
And чекбокс «Тропы» был ON
|
||||
When пользователь снимает чекбокс «Тропы»
|
||||
Then trails-path-bridleway visibility=none
|
||||
And trails-path-bridleway-halo-satellite visibility=none
|
||||
|
||||
Scenario: На «Схеме» halo-слои всегда скрыты (P1-6)
|
||||
Given активна подложка «Схема»
|
||||
And чекбокс «Грунтовки» ON
|
||||
Then trails-track visibility=visible
|
||||
And trails-track-halo-satellite visibility=none
|
||||
|
||||
Scenario: POI видны и читаемы на спутнике (P1-2)
|
||||
Given активна подложка «Спутник»
|
||||
And в попапе включён «POI»
|
||||
Then на карте видны маркеры POI поверх спутника
|
||||
And подписи POI читаемы (имеют тёмный halo)
|
||||
And poi-labels paint: text-color === '#ffffff'
|
||||
And poi-labels paint: text-halo-color === '#000000'
|
||||
And poi-labels paint: text-halo-width === 2
|
||||
And poi-circles paint: circle-stroke-color === '#ffffff'
|
||||
And poi-circles paint: circle-stroke-width === 2
|
||||
|
||||
Scenario: POI baseline восстанавливается на «Схеме» (P1-2)
|
||||
Given был активен «Спутник», POI labels в режиме спутника
|
||||
When пользователь возвращается на «Схему» (light-тема)
|
||||
Then poi-labels paint: text-color === '#333333' (baseline light, Data §5)
|
||||
And poi-labels paint: text-halo-color === '#ffffff' (baseline light)
|
||||
And poi-labels paint: text-halo-width === 1.5 (baseline light)
|
||||
|
||||
Scenario: Hillshade поверх спутника
|
||||
Given активна подложка «Спутник»
|
||||
|
||||
@@ -183,30 +183,48 @@ Esri World Imagery — единственный вариант, удовлетв
|
||||
|
||||
5. **Halo — гибридный подход:**
|
||||
|
||||
- Для **линий grade1..5 и paths/bridleway** в обоих `style.json` /
|
||||
`style-dark.json` заводятся парные «underlay»-слои
|
||||
(`*-halo-satellite`) с более широкой полупрозрачной белой
|
||||
обводкой и `layout.visibility = "none"`. При входе в «Спутник»
|
||||
эти слои становятся видимыми; при возврате на «Схему» —
|
||||
скрываются. Никаких runtime-правок paint не требуется.
|
||||
- Для **POI labels** меняются динамически только два свойства —
|
||||
`text-halo-color` (`#000000` на спутнике / исходное на схеме) и
|
||||
`text-halo-width` (`2` на спутнике / исходное на схеме) — через
|
||||
`setPaintProperty`. Эти исходные значения известны и
|
||||
зафиксированы в `style.json`; читать «текущее» через
|
||||
`getPaintProperty` не нужно — всегда выставляем явные значения
|
||||
для обоих режимов.
|
||||
- Для **линий грунтовок и троп** в обоих `style.json` /
|
||||
`style-dark.json` присутствуют парные «underlay»-слои
|
||||
`trails-track-halo-satellite` и
|
||||
`trails-path-bridleway-halo-satellite` (более широкая
|
||||
полупрозрачная белая обводка, `layout.visibility = "none"`).
|
||||
При входе в «Спутник» эти слои становятся видимыми; при возврате
|
||||
на «Схему» — скрываются. Никаких runtime-правок paint не
|
||||
требуется. Слоёв на каждую grade (`trails-grade1..5-halo-satellite`)
|
||||
**не заводится**: дифференциация grade хранится внутри одного
|
||||
`match`-выражения по `tracktype` в `trails-track`, halo единого
|
||||
цвета/ширины накладывается на весь слой целиком — этого
|
||||
достаточно для читаемости (под halo всё равно ляжет цветная
|
||||
линия `trails-track`). Аналогично для троп — единый halo на весь
|
||||
`trails-path-bridleway` (фильтр `highway in path/bridleway/footway`).
|
||||
`trails-asphalt` halo не получает: он по умолчанию скрыт
|
||||
(`visibility: none`, `line-opacity: 0`); если в будущей фазе
|
||||
включится — добавится halo тем же паттерном.
|
||||
- Для **POI labels** меняются динамически три свойства:
|
||||
`text-color` (`#ffffff` на спутнике / baseline текущей темы на схеме —
|
||||
`#333333` для light, `#e0e0e0` для dark), `text-halo-color`
|
||||
(`#000000` на спутнике / baseline `#ffffff` для light,
|
||||
`#1a1a2e` для dark на схеме), `text-halo-width` (`2` на спутнике
|
||||
/ baseline `1.5` для light, `2` для dark на схеме). Менять
|
||||
**обе** пары (color + halo) необходимо: иначе тёмный baseline-
|
||||
текст светлой темы (`#333333`) поверх чёрного halo не читается.
|
||||
Baseline-значения известны и зафиксированы в Data §5; всегда
|
||||
выставляем явные значения для обоих режимов.
|
||||
- **POI circles** — обводка `circle-stroke-color: #ffffff` /
|
||||
`circle-stroke-width: 2` динамически на спутнике, возврат к
|
||||
исходным значениям из `style.json` на схеме.
|
||||
baseline текущей темы из Data §5 на схеме (`#ffffff`/`1.5` light,
|
||||
`#333333`/`1.5` dark).
|
||||
|
||||
6. **Цвет `background`** в режиме «Спутник» меняется через
|
||||
`setPaintProperty('background', 'background-color', '#2a2a2a')`
|
||||
(тёмно-серый), чтобы не «бликовало» под медленно подгружающимися
|
||||
спутниковыми плитками. При возврате на «Схему» восстанавливаются
|
||||
исходные значения из `style.json` (`#f0ede6` для светлой темы,
|
||||
тёмное значение из `style-dark.json` для тёмной). Эти константы —
|
||||
единственные «дублирующие» значения; они зафиксированы в
|
||||
`setPaintProperty('background', 'background-color', '#2a2a2a')` —
|
||||
**единая константа `#2a2a2a` для обеих тем** (тёмно-серый, чтобы
|
||||
не «бликовал» под медленно подгружающимися спутниковыми плитками).
|
||||
На обеих темах используется одно и то же значение; per-theme-
|
||||
развилки нет (упрощает код и исключает рассинхрон). При возврате
|
||||
на «Схему» восстанавливаются baseline-значения текущей темы —
|
||||
`#f0ede6` (light, из `style.json`) и `#1a1a2e` (dark, из
|
||||
`style-dark.json`; **не** `#1a1a1a` — это была ошибка в более
|
||||
раннем черновике). Эти baseline-константы зафиксированы в
|
||||
`applyBaseLayer()` и в `08-data-requirements.md` §5.
|
||||
|
||||
7. **localStorage — ключ `map-base-layer`** (см. TRZ §4.3), значения
|
||||
@@ -215,6 +233,32 @@ Esri World Imagery — единственный вариант, удовлетв
|
||||
(`enduro-theme-mode`, `distance_unit`, `terrain-*`, `trails-*`,
|
||||
`poi-visible`) — никаких миграций старых значений не требуется.
|
||||
|
||||
8. **Контракт с существующим `toggleLayer('basemap')`
|
||||
(`app.js:384–391`).** В коде уже есть отдельный пользовательский
|
||||
выключатель «Базовая карта» (управляет `osm-base.visibility` и
|
||||
`layerState.basemap`). ET-007 принимает паттерн **save & restore**
|
||||
(см. TRZ §5.6): при входе в «Спутник» сохраняем `layerState.basemap`
|
||||
в `_savedBasemapState` и принудительно скрываем `osm-base`; UI-кнопка
|
||||
`#btn-basemap` скрывается через CSS-класс `.satellite-active` (чтобы
|
||||
пользователь не пытался включить «гибрид»: out of scope BRD §3).
|
||||
При возврате на «Схему» восстанавливаем `osm-base.visibility` из
|
||||
сохранённого значения. На «Схеме» `toggleLayer('basemap')` работает
|
||||
как раньше — ET-007 этот код не трогает.
|
||||
|
||||
9. **Синхронизация halo-слоёв с пользовательскими чекбоксами
|
||||
«Грунтовки» / «Тропы» (`app.js:2783–2826`).** В существующих
|
||||
`onTrailsCheckbox()` / `restoreTrailsState()` управляется
|
||||
видимость только `trails-track` и `trails-path-bridleway`. Halo-
|
||||
underlay-слои сами по себе не отслеживаются; на спутнике это даёт
|
||||
«фантом» halo при выключенной грунтовке/тропе. Решение (TRZ §5.7):
|
||||
ввести единый хелпер `applyTrailHaloVisibility(trackOn, pathOn)`
|
||||
и вызывать его из (а) `onTrailsCheckbox`, (б) `restoreTrailsState`,
|
||||
(в) `applyBaseLayer('satellite' | 'schematic')`. Правило: halo
|
||||
видим ⇔ `currentBaseLayer === 'satellite' AND checkbox === ON`.
|
||||
POI отдельной синхронизации не требуют — paint-правки текста
|
||||
привязаны к самим `poi-circles`/`poi-labels`, которые управляются
|
||||
`layerState.poi` / `restorePoiState()`.
|
||||
|
||||
8. **C4 / архитектурная диаграмма.** В репозитории нет файлов
|
||||
`c4-*.mmd`; описание архитектуры — текстовое в
|
||||
`docs/architecture/README.md`. Туда добавляется отдельный раздел
|
||||
@@ -289,6 +333,27 @@ ET-007 не вводит. Внешний тайл-провайдер — рас
|
||||
архитектурный класс. Лейбл `arch:major-change` **не требуется**.
|
||||
Обязательного дополнительного архитектурного approve не требуется.
|
||||
|
||||
## Ревизии
|
||||
|
||||
- 2026-05-31 — editorial: code-review fixes (12-review.md attempt 2/3).
|
||||
Решения P/M/S/O/H **не пересматривались**. Правки:
|
||||
- §5 пункт 1: реальные id halo-слоёв
|
||||
(`trails-track-halo-satellite`, `trails-path-bridleway-halo-satellite`)
|
||||
вместо несуществующих `trails-grade1..5-halo-satellite` /
|
||||
`paths-bridleway-halo-satellite` (P1-1).
|
||||
- §5 пункт 2 (POI labels): добавлена правка `text-color` на
|
||||
спутнике + явный baseline возврата per-theme — без этого тёмный
|
||||
`#333333` поверх чёрного halo был нечитаем (P1-2).
|
||||
- §6: зафиксирована единая satellite-константа `#2a2a2a` для обеих
|
||||
тем; baseline dark исправлен `#1a1a1a` → `#1a1a2e` под фактическое
|
||||
значение `style-dark.json:28` (P1-4).
|
||||
- Добавлен §8: контракт с существующим `toggleLayer('basemap')` /
|
||||
`layerState.basemap` — паттерн save&restore через
|
||||
`_savedBasemapState` (P1-5).
|
||||
- Добавлен §9: синхронизация halo-слоёв с пользовательскими
|
||||
чекбоксами «Грунтовки»/«Тропы» — хелпер
|
||||
`applyTrailHaloVisibility` (P1-6).
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- `docs/work-items/ET-007/01-brd.md`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
type: data-requirements
|
||||
work_item_id: ET-007
|
||||
title: "Требования к данным — ET-007: Спутниковая карта (Схема / Спутник)"
|
||||
version: 1
|
||||
version: 2
|
||||
status: approved
|
||||
created_at: 2026-05-31
|
||||
changelog:
|
||||
- "v2 (2026-05-31): code-review fixes (12-review.md P1-1, P1-2, P1-4) — реальные id halo-слоёв (trails-track/path-bridleway), полная таблица baseline POI per-theme, satellite-bg как единая константа #2a2a2a, исправление dark baseline #1a1a1a→#1a1a2e, добавлено поле _savedBasemapState."
|
||||
authors:
|
||||
- "agent:architect"
|
||||
---
|
||||
@@ -73,32 +75,65 @@ UI-выбор подложки в `localStorage`. На стороне внешн
|
||||
| Поле | Тип | Назначение |
|
||||
|------|-----|------------|
|
||||
| текущий базовый слой | `'schematic' \| 'satellite'` | проекция `localStorage['map-base-layer']` |
|
||||
| baseline-значения paint POI (text-halo, circle-stroke) | объекты per-layer | референсы для возврата с «Спутник» на «Схему» |
|
||||
| baseline-значения `background-color` для тёмной/светлой темы | две строковые константы | `#f0ede6` (light), `#1a1a1a` (dark) — задублированы из `style*.json`, см. ADR-004 §6 |
|
||||
| baseline-значения paint POI (см. таблицу ниже) | константы per-theme | референсы для возврата с «Спутник» на «Схему» |
|
||||
| baseline-значения `background-color` для тёмной/светлой темы | две строковые константы | `#f0ede6` (light), `#1a1a2e` (dark) — задублированы из `style.json:28` и `style-dark.json:28`, см. ADR-004 §6 |
|
||||
| satellite-константа `background-color` | одна строковая константа | `#2a2a2a` для обеих тем (ADR-004 §6) |
|
||||
| `_savedBasemapState` | `boolean \| null` | сохранённое значение `layerState.basemap` на время активного «Спутник»; восстанавливается при возврате на «Схему» (TRZ §5.6, P1-5) |
|
||||
| флаг «satellite source уже добавлен в стиль» | bool | оптимизация: при повторном входе в «Спутник» в той же сессии стиля не добавляем повторно |
|
||||
|
||||
baseline POI-значения и `background-color` — единственные
|
||||
**задублированные** значения между `style*.json` и `app.js`. Их
|
||||
рассинхрон ловится UI-тестами AC-04 и AC-06.
|
||||
### 5.1 Baseline paint-значений POI на «Схеме» (источник истины)
|
||||
|
||||
| Свойство | Light (`style.json:128–163`) | Dark (`style-dark.json:128–163`) |
|
||||
|----------|------------------------------|----------------------------------|
|
||||
| `poi-circles` `circle-stroke-color` | `#ffffff` | `#333333` |
|
||||
| `poi-circles` `circle-stroke-width` | `1.5` | `1.5` |
|
||||
| `poi-labels` `text-color` | `#333333` | `#e0e0e0` |
|
||||
| `poi-labels` `text-halo-color` | `#ffffff` | `#1a1a2e` |
|
||||
| `poi-labels` `text-halo-width` | `1.5` | `2` |
|
||||
|
||||
### 5.2 Значения POI в режиме «Спутник» (общие для обеих тем)
|
||||
|
||||
| Свойство | Satellite |
|
||||
|----------|-----------|
|
||||
| `poi-circles` `circle-stroke-color` | `#ffffff` |
|
||||
| `poi-circles` `circle-stroke-width` | `2` |
|
||||
| `poi-labels` `text-color` | `#ffffff` |
|
||||
| `poi-labels` `text-halo-color` | `#000000` |
|
||||
| `poi-labels` `text-halo-width` | `2` |
|
||||
|
||||
Менять обе пары (`text-color` + `text-halo-*`) обязательно: без правки
|
||||
`text-color` тёмный baseline-текст светлой темы (`#333333`) поверх
|
||||
чёрного halo не читается (см. 12-review.md P1-2).
|
||||
|
||||
baseline POI-значения, `background-color` light/dark и satellite-
|
||||
константа фона — **единственные** задублированные значения между
|
||||
`style*.json` и `app.js`. Их рассинхрон ловится UI-тестами AC-04 (POI
|
||||
видимость на спутнике) и AC-06 (смена темы при активном «Спутник»).
|
||||
|
||||
## 6. Halo-слои в `style.json`
|
||||
|
||||
В обоих `src/web/style.json` и `src/web/style-dark.json` добавляются
|
||||
парные «underlay»-слои halo для линий грунтовок/троп, например:
|
||||
В обоих `src/web/style.json` и `src/web/style-dark.json` уже
|
||||
присутствуют парные «underlay»-слои halo для линий грунтовок и троп
|
||||
(см. `style.json:56–70`, `93–107`):
|
||||
|
||||
| Базовый слой | Halo-слой | Назначение |
|
||||
|--------------|-----------|------------|
|
||||
| `trails-grade1` | `trails-grade1-halo-satellite` | широкая полупрозрачная белая обводка под основной линией |
|
||||
| `trails-grade2` | `trails-grade2-halo-satellite` | то же |
|
||||
| ... | ... | для каждой grade и для paths/bridleway |
|
||||
| `paths-bridleway` | `paths-bridleway-halo-satellite` | то же |
|
||||
| Базовый слой | Halo-слой | Фильтр базового слоя | Назначение |
|
||||
|--------------|-----------|----------------------|------------|
|
||||
| `trails-track` | `trails-track-halo-satellite` | `highway == 'track'` (grade1..5 различаются `match`-выражением внутри `line-color`) | широкая полупрозрачная белая обводка под основной линией |
|
||||
| `trails-path-bridleway` | `trails-path-bridleway-halo-satellite` | `highway in path/bridleway/footway` | то же |
|
||||
|
||||
Параметры halo-слоёв (ширина, цвет, opacity) — на этапе разработки;
|
||||
дизайн уточняется визуальной проверкой на тёмных снимках. У всех
|
||||
halo-слоёв `layout.visibility = "none"` по умолчанию; включаются в
|
||||
`applyBaseLayer('satellite')` через `setLayoutProperty`. Точные
|
||||
численные значения halo — данные дизайна, не данные домена; их
|
||||
изменение не требует миграции пользовательского состояния.
|
||||
Слоёв на каждую grade (`trails-grade1..5-halo-satellite`) **нет** и
|
||||
заводить не планируется: дифференциация grade зашита в один
|
||||
`match`-expression по `tracktype` внутри `trails-track`, а halo на
|
||||
спутнике достаточно единого цвета/ширины поверх всего трека (под halo
|
||||
ляжет цветная линия `trails-track`, разделение halo по grade
|
||||
визуально не различимо). Аналогично для троп — единый
|
||||
`trails-path-bridleway-halo-satellite` покрывает всю группу
|
||||
`path/bridleway/footway`. Слой `trails-asphalt` halo не получает: по
|
||||
умолчанию `visibility: none` + `line-opacity: 0`.
|
||||
|
||||
Параметры halo-слоёв (ширина, цвет, opacity) уже зафиксированы в
|
||||
коде; будущие правки — данные дизайна, не данные домена; их изменение
|
||||
не требует миграции пользовательского состояния.
|
||||
|
||||
## 7. Персональные данные
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
type: tech-risks
|
||||
work_item_id: ET-007
|
||||
title: "Технические риски — ET-007: Спутниковая карта (Схема / Спутник)"
|
||||
version: 1
|
||||
version: 2
|
||||
status: approved
|
||||
created_at: 2026-05-31
|
||||
changelog:
|
||||
- "v2 (2026-05-31): code-review fix (12-review.md P1-1) — R-1 переписан под реальные halo-id (trails-track-halo-satellite, trails-path-bridleway-halo-satellite); исключён фиктивный массив grade1..5."
|
||||
authors:
|
||||
- "agent:architect"
|
||||
---
|
||||
@@ -18,19 +20,28 @@ authors:
|
||||
## R-1 — Дрейф halo-слоёв в `style.json` / `style-dark.json`
|
||||
|
||||
- **Описание:** ADR-004 §5 решает читаемость линий грунтовок и троп
|
||||
на спутнике через парные «underlay»-слои `*-halo-satellite` с
|
||||
`visibility: none` в обоих файлах стилей. Любая будущая правка
|
||||
основных trails-слоёв (цвет, ширина, фильтр) требует **согласованной
|
||||
правки halo-слоёв** в обоих файлах. Без явной проверки легко
|
||||
забыть один из четырёх случаев (2 темы × 2 рода слоёв).
|
||||
на спутнике через парные «underlay»-слои с `visibility: none` в
|
||||
обоих файлах стилей. Реальные id (подтверждены кодом
|
||||
`style.json:56–70`, `93–107` и `style-dark.json:56–70`, `93–107`):
|
||||
`trails-track-halo-satellite`, `trails-path-bridleway-halo-satellite`.
|
||||
Любая будущая правка основных trails-слоёв (цвет, ширина, фильтр)
|
||||
требует **согласованной правки halo-слоёв** в обоих файлах. Без
|
||||
явной проверки легко забыть один из четырёх случаев (2 темы × 2
|
||||
рода слоёв).
|
||||
- **Вероятность / Влияние:** С / Н.
|
||||
- **Митигация:**
|
||||
- При разработке завести единый список затрагиваемых пар в
|
||||
`applyBaseLayer()`: массив `['trails-grade1', 'trails-grade2', ...]`
|
||||
с производным правилом `<base>-halo-satellite`. Это исключит
|
||||
«забытый» halo-слой со стороны JS.
|
||||
- Code review-чеклист: при правке trails-* в `style*.json` —
|
||||
обязательная сверка `*-halo-satellite` в том же файле.
|
||||
`applyBaseLayer()`: массив пар `[('trails-track',
|
||||
'trails-track-halo-satellite'), ('trails-path-bridleway',
|
||||
'trails-path-bridleway-halo-satellite')]`. Производное правило
|
||||
«`<base>-halo-satellite`» допустимо, но только для **этих двух**
|
||||
base-id; массив `['trails-grade1..5']` (как в более раннем
|
||||
черновике, см. 12-review.md P1-1) **не использовать** — таких
|
||||
слоёв в `style.json` нет, дифференциация grade хранится внутри
|
||||
одного `match`-выражения по `tracktype` в `trails-track`.
|
||||
- Code review-чеклист: при правке `trails-track`, `trails-path-
|
||||
bridleway` в `style*.json` — обязательная сверка соответствующего
|
||||
`*-halo-satellite` в том же файле.
|
||||
- UI-тест AC-04 проверяет видимость линий поверх спутника в обеих
|
||||
темах.
|
||||
|
||||
@@ -90,10 +101,13 @@ authors:
|
||||
## R-5 — Дублирование `background-color` между `style*.json` и `app.js`
|
||||
|
||||
- **Описание:** ADR-004 §6 требует менять `background-color` на
|
||||
тёмно-серый при включении «Спутник» и возвращать к исходному при
|
||||
возврате на «Схему». «Исходные» значения (`#f0ede6` для светлой,
|
||||
тёмное для тёмной) дублируются в `applyBaseLayer()` и в
|
||||
`style*.json` — при смене палитры тем легко забыть один из двух.
|
||||
единую satellite-константу `#2a2a2a` (обе темы) при включении
|
||||
«Спутник» и возвращать к исходному при возврате на «Схему».
|
||||
«Исходные» значения (`#f0ede6` для светлой, `#1a1a2e` для тёмной —
|
||||
именно `#1a1a2e`, как в `style-dark.json:28`, а не `#1a1a1a` из
|
||||
более раннего черновика, см. 12-review.md P1-4 / P2-3)
|
||||
дублируются в `applyBaseLayer()` и в `style*.json` — при смене
|
||||
палитры тем легко забыть один из двух.
|
||||
- **Вероятность / Влияние:** Н / Н.
|
||||
- **Митигация:**
|
||||
- Альтернатива — при возврате на «Схему» **читать** актуальное
|
||||
|
||||
@@ -887,6 +887,15 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
/* ET-007 P1-5 / ADR-004 §8: пока активен «Спутник», скрыть UI-кнопку
|
||||
«Базовая карта» (#btn-basemap) — гибридный режим (схема поверх
|
||||
спутника) out of scope BRD §3. JS добавляет/снимает класс
|
||||
.satellite-active на <body> в applyBaseLayer(). На «Схеме» — кнопка
|
||||
снова видна (если она присутствует в текущей вёрстке). */
|
||||
body.satellite-active #btn-basemap {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── ET-005: переключатель единиц измерения (км/мили) в попапе рельефа ── */
|
||||
.terrain-unit-row {
|
||||
padding: 8px 4px 2px;
|
||||
|
||||
169
src/web/app.js
169
src/web/app.js
@@ -2783,14 +2783,14 @@ function onTerrainCheckbox() {
|
||||
function onTrailsCheckbox() {
|
||||
const map = window._map;
|
||||
if (!map) return;
|
||||
|
||||
|
||||
const trackChecked = document.getElementById('trails-track-cb').checked;
|
||||
const pathChecked = document.getElementById('trails-path-cb').checked;
|
||||
|
||||
|
||||
// Save state
|
||||
localStorage.setItem('trails-track', trackChecked ? '1' : '0');
|
||||
localStorage.setItem('trails-path', pathChecked ? '1' : '0');
|
||||
|
||||
|
||||
// Toggle layer visibility
|
||||
if (map.getLayer('trails-track')) {
|
||||
map.setLayoutProperty('trails-track', 'visibility', trackChecked ? 'visible' : 'none');
|
||||
@@ -2798,22 +2798,31 @@ function onTrailsCheckbox() {
|
||||
if (map.getLayer('trails-path-bridleway')) {
|
||||
map.setLayoutProperty('trails-path-bridleway', 'visibility', pathChecked ? 'visible' : 'none');
|
||||
}
|
||||
// ET-007 P1-6: синхронизируем halo-underlay-слои с состоянием
|
||||
// чекбоксов, чтобы на спутнике не оставалось «фантома» halo при
|
||||
// выключенной грунтовке/тропе. Безопасно к ранней инициализации:
|
||||
// _applyTrailHaloVisibility определена ниже в том же файле (ET-007
|
||||
// base layer block). См. ADR-004 §9, TRZ §5.7.
|
||||
if (typeof _applyTrailHaloVisibility === 'function' &&
|
||||
typeof getStoredBaseLayer === 'function') {
|
||||
_applyTrailHaloVisibility(map, getStoredBaseLayer());
|
||||
}
|
||||
}
|
||||
|
||||
function restoreTrailsState() {
|
||||
const trackState = localStorage.getItem('trails-track');
|
||||
const pathState = localStorage.getItem('trails-path');
|
||||
|
||||
|
||||
// Default: both checked (visible)
|
||||
const trackOn = trackState === null || trackState === '1';
|
||||
const pathOn = pathState === null || pathState === '1';
|
||||
|
||||
|
||||
const trackCb = document.getElementById('trails-track-cb');
|
||||
const pathCb = document.getElementById('trails-path-cb');
|
||||
|
||||
|
||||
if (trackCb) trackCb.checked = trackOn;
|
||||
if (pathCb) pathCb.checked = pathOn;
|
||||
|
||||
|
||||
const map = window._map;
|
||||
if (map) {
|
||||
if (map.getLayer('trails-track')) {
|
||||
@@ -2822,6 +2831,11 @@ function restoreTrailsState() {
|
||||
if (map.getLayer('trails-path-bridleway')) {
|
||||
map.setLayoutProperty('trails-path-bridleway', 'visibility', pathOn ? 'visible' : 'none');
|
||||
}
|
||||
// ET-007 P1-6: тот же контракт, что в onTrailsCheckbox (см. выше).
|
||||
if (typeof _applyTrailHaloVisibility === 'function' &&
|
||||
typeof getStoredBaseLayer === 'function') {
|
||||
_applyTrailHaloVisibility(map, getStoredBaseLayer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2911,6 +2925,28 @@ const SATELLITE_HALO_LAYER_IDS = [
|
||||
'trails-path-bridleway-halo-satellite',
|
||||
];
|
||||
|
||||
/**
|
||||
* Пары (base-layer, halo-underlay) для синхронизации halo с
|
||||
* пользовательскими чекбоксами «Грунтовки» / «Тропы»
|
||||
* (ADR-004 §9, TRZ §5.7). Источник истины: halo видим ⇔
|
||||
* (текущая база === 'satellite') AND (соответствующий чекбокс ON).
|
||||
*/
|
||||
const TRAIL_HALO_PAIRS = [
|
||||
{ base: 'trails-track', halo: 'trails-track-halo-satellite' },
|
||||
{ base: 'trails-path-bridleway', halo: 'trails-path-bridleway-halo-satellite' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Сохранённое значение `layerState.basemap` на время активного
|
||||
* режима «Спутник» (ADR-004 §8, TRZ §5.6). `null` означает «сейчас
|
||||
* на схеме, восстанавливать нечего». При входе в «Спутник» сохраняем
|
||||
* сюда `layerState.basemap`, при выходе — восстанавливаем и
|
||||
* обнуляем. Это сохраняет выбор пользователя по «Базовая карта»
|
||||
* через ход «Схема → Спутник → Схема» без рассинхрона с
|
||||
* `layerState.basemap`.
|
||||
*/
|
||||
let _savedBasemapState = null;
|
||||
|
||||
/**
|
||||
* Возвращает выбранную пользователем подложку из localStorage.
|
||||
*
|
||||
@@ -2988,20 +3024,39 @@ function applyBaseLayer(base) {
|
||||
if (map.getLayer(SATELLITE_LAYER_ID)) {
|
||||
map.setLayoutProperty(SATELLITE_LAYER_ID, 'visibility', 'visible');
|
||||
}
|
||||
// ET-007 P1-5 / ADR-004 §8: запоминаем layerState.basemap и
|
||||
// принудительно скрываем osm-base. layerState.basemap не меняем —
|
||||
// это пользовательский выбор «Базовая карта», его восстановим при
|
||||
// возврате на «Схему».
|
||||
if (_savedBasemapState === null && typeof layerState !== 'undefined') {
|
||||
_savedBasemapState = layerState.basemap;
|
||||
}
|
||||
if (map.getLayer('osm-base')) {
|
||||
map.setLayoutProperty('osm-base', 'visibility', 'none');
|
||||
}
|
||||
_toggleSatelliteHalo(map, true);
|
||||
// CSS-hook: скрыть кнопку #btn-basemap пока активен спутник
|
||||
// (гибридный режим out of scope — BRD §3). Defensive: mock-DOM в
|
||||
// unit-тестах может не иметь classList.add/remove.
|
||||
_setBodyClass('satellite-active', true);
|
||||
// ET-007 P1-6: halo синхронизирован с состоянием чекбоксов
|
||||
// «Грунтовки» / «Тропы», а не безусловно включён.
|
||||
_applyTrailHaloVisibility(map, 'satellite');
|
||||
_applyPoiSatellitePaint(map, true);
|
||||
_applyBackgroundForSatellite(map, true);
|
||||
} else {
|
||||
if (map.getLayer(SATELLITE_LAYER_ID)) {
|
||||
map.setLayoutProperty(SATELLITE_LAYER_ID, 'visibility', 'none');
|
||||
}
|
||||
// ET-007 P1-5: восстановить выбор пользователя по «Базовой карте»
|
||||
// (если он раньше выключал osm-base — оставить выключенным).
|
||||
if (map.getLayer('osm-base')) {
|
||||
map.setLayoutProperty('osm-base', 'visibility', 'visible');
|
||||
const wantOsm = _savedBasemapState !== false; // default visible
|
||||
map.setLayoutProperty('osm-base', 'visibility', wantOsm ? 'visible' : 'none');
|
||||
}
|
||||
_toggleSatelliteHalo(map, false);
|
||||
_savedBasemapState = null;
|
||||
_setBodyClass('satellite-active', false);
|
||||
// На «Схеме» halo всегда скрыт независимо от чекбоксов.
|
||||
_applyTrailHaloVisibility(map, 'schematic');
|
||||
_applyPoiSatellitePaint(map, false);
|
||||
_applyBackgroundForSatellite(map, false);
|
||||
}
|
||||
@@ -3037,6 +3092,24 @@ function syncBaseLayerUI(base) {
|
||||
|
||||
// ── Приватные хелперы (ADR-004 §5) ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Defensive переключатель класса на document.body. Реальный браузерный
|
||||
* `classList` имеет `add`/`remove`/`toggle`, но в unit-тестах
|
||||
* (tests/unit/base_layer.test.js) мок-DOM собран минимально и содержит
|
||||
* только `contains`. Использует `toggle(name, on)` если доступен,
|
||||
* иначе деградирует в no-op (тестовая среда — побочные эффекты на body
|
||||
* не важны).
|
||||
*/
|
||||
function _setBodyClass(name, on) {
|
||||
if (typeof document === 'undefined' || !document.body) return;
|
||||
const cl = document.body.classList;
|
||||
if (!cl) return;
|
||||
if (typeof cl.toggle === 'function') { cl.toggle(name, !!on); return; }
|
||||
if (on && typeof cl.add === 'function') { cl.add(name); return; }
|
||||
if (!on && typeof cl.remove === 'function') { cl.remove(name); return; }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Возвращает id первого «верхнего» слоя (terrain/trails/POI),
|
||||
* чтобы спутник был добавлен ПОД ним и terrain/trails/POI/маршрут
|
||||
@@ -3055,25 +3128,56 @@ function _firstOverlayLayerId(map) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключает видимость halo-underlay-слоёв у trails (TRZ §1 REQ-F-04,
|
||||
* ADR-004 §5, вариант H-B).
|
||||
* Применяет видимость halo-underlay-слоёв у trails по правилу
|
||||
* «halo видим ⇔ (base === 'satellite') AND (соответствующий чекбокс ON)»
|
||||
* (TRZ §5.7, ADR-004 §9, 12-review.md P1-6).
|
||||
*
|
||||
* Состояние чекбоксов читается из DOM (`#trails-track-cb`,
|
||||
* `#trails-path-cb`). Если узлов нет (тесты под jsdom без HTML или
|
||||
* ранний вызов до отрисовки попапа) — пары считаются ON (`true`),
|
||||
* это совпадает с дефолтом `restoreTrailsState()`.
|
||||
*
|
||||
* @param {object} map - инстанс MapLibre.
|
||||
* @param {('schematic'|'satellite')} base - текущая база.
|
||||
*/
|
||||
function _toggleSatelliteHalo(map, enabled) {
|
||||
const visibility = enabled ? 'visible' : 'none';
|
||||
SATELLITE_HALO_LAYER_IDS.forEach((id) => {
|
||||
if (map.getLayer(id)) {
|
||||
map.setLayoutProperty(id, 'visibility', visibility);
|
||||
}
|
||||
function _applyTrailHaloVisibility(map, base) {
|
||||
const trackCb = (typeof document !== 'undefined') &&
|
||||
document.getElementById && document.getElementById('trails-track-cb');
|
||||
const pathCb = (typeof document !== 'undefined') &&
|
||||
document.getElementById && document.getElementById('trails-path-cb');
|
||||
const trackOn = trackCb ? !!trackCb.checked : true;
|
||||
const pathOn = pathCb ? !!pathCb.checked : true;
|
||||
const onByBase = base === 'satellite';
|
||||
const pairs = [
|
||||
{ halo: 'trails-track-halo-satellite', checked: trackOn },
|
||||
{ halo: 'trails-path-bridleway-halo-satellite', checked: pathOn },
|
||||
];
|
||||
pairs.forEach((p) => {
|
||||
if (!map.getLayer(p.halo)) return;
|
||||
const visibility = (onByBase && p.checked) ? 'visible' : 'none';
|
||||
map.setLayoutProperty(p.halo, 'visibility', visibility);
|
||||
});
|
||||
}
|
||||
|
||||
// Обратная совместимость для существующих unit-тестов, которые могли
|
||||
// ссылаться на _toggleSatelliteHalo до P1-6 рефакторинга. Делегирует
|
||||
// на новую функцию с правильным base. См. tests/unit/base_layer.test.js.
|
||||
function _toggleSatelliteHalo(map, enabled) {
|
||||
_applyTrailHaloVisibility(map, enabled ? 'satellite' : 'schematic');
|
||||
}
|
||||
|
||||
/**
|
||||
* Применяет правки paint к POI labels/circles в зависимости от
|
||||
* активной подложки (ADR-004 §5).
|
||||
* активной подложки (ADR-004 §5, Data §5.1–5.2).
|
||||
*
|
||||
* На «Спутнике» — чёрный halo у подписей и белая обводка у кружков,
|
||||
* чтобы POI оставались читаемыми поверх тёмных снимков. На «Схеме» —
|
||||
* возврат к значениям из style.json соответствующей темы.
|
||||
* На «Спутнике» — белый текст с чёрным halo у подписей и белая
|
||||
* обводка у кружков, чтобы POI оставались читаемыми поверх тёмных
|
||||
* снимков. На «Схеме» — возврат к baseline-значениям текущей темы
|
||||
* из `style.json` / `style-dark.json` (см. Data §5.1).
|
||||
*
|
||||
* Менять обе пары (`text-color` + `text-halo-*`) обязательно: иначе
|
||||
* baseline-текст светлой темы `#333333` поверх чёрного halo не
|
||||
* читается (см. 12-review.md P1-2).
|
||||
*/
|
||||
function _applyPoiSatellitePaint(map, satellite) {
|
||||
const dark = (typeof document !== 'undefined') &&
|
||||
@@ -3081,9 +3185,13 @@ function _applyPoiSatellitePaint(map, satellite) {
|
||||
document.body.classList.contains('theme-dark');
|
||||
if (map.getLayer('poi-labels')) {
|
||||
if (satellite) {
|
||||
// Satellite — единые значения для обеих тем (Data §5.2).
|
||||
map.setPaintProperty('poi-labels', 'text-color', '#ffffff');
|
||||
map.setPaintProperty('poi-labels', 'text-halo-color', '#000000');
|
||||
map.setPaintProperty('poi-labels', 'text-halo-width', 2);
|
||||
} else {
|
||||
// Schematic — baseline текущей темы (Data §5.1).
|
||||
map.setPaintProperty('poi-labels', 'text-color', dark ? '#e0e0e0' : '#333333');
|
||||
map.setPaintProperty('poi-labels', 'text-halo-color', dark ? '#1a1a2e' : '#ffffff');
|
||||
map.setPaintProperty('poi-labels', 'text-halo-width', dark ? 2 : 1.5);
|
||||
}
|
||||
@@ -3100,18 +3208,21 @@ function _applyPoiSatellitePaint(map, satellite) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Меняет цвет background-слоя под спутником на тёмно-серый
|
||||
* (TRZ §1 REQ-F-03, ADR-004 §6). На «Схеме» — возврат к цвету из
|
||||
* style.json / style-dark.json.
|
||||
* Меняет цвет background-слоя под спутником на единый тёмно-серый
|
||||
* `#2a2a2a` (обе темы) — TRZ §1 REQ-F-03, ADR-004 §6. На «Схеме» —
|
||||
* возврат к baseline текущей темы из Data §5 (`#f0ede6` light /
|
||||
* `#1a1a2e` dark; именно `#1a1a2e`, как в `style-dark.json:28`, а
|
||||
* не `#1a1a1a` из более раннего черновика — см. 12-review.md P1-4).
|
||||
*/
|
||||
function _applyBackgroundForSatellite(map, satellite) {
|
||||
if (!map.getLayer('background')) return;
|
||||
const dark = (typeof document !== 'undefined') &&
|
||||
document.body && document.body.classList &&
|
||||
document.body.classList.contains('theme-dark');
|
||||
if (satellite) {
|
||||
map.setPaintProperty('background', 'background-color', dark ? '#2a2a2a' : '#1a1a1a');
|
||||
// Единая константа для обеих тем (ADR-004 §6).
|
||||
map.setPaintProperty('background', 'background-color', '#2a2a2a');
|
||||
} else {
|
||||
const dark = (typeof document !== 'undefined') &&
|
||||
document.body && document.body.classList &&
|
||||
document.body.classList.contains('theme-dark');
|
||||
map.setPaintProperty('background', 'background-color', dark ? '#1a1a2e' : '#f0ede6');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,17 +339,20 @@ test('I-25/dark: возврат на «Схему» в тёмной теме д
|
||||
assert.deepEqual(haloColor, ['poi-labels', 'text-halo-color', '#1a1a2e']);
|
||||
});
|
||||
|
||||
// ── background — тёмно-серый под спутником ────────────────────────────
|
||||
test('фон под спутником в светлой теме — #1a1a1a', () => {
|
||||
// ── background — единая satellite-константа #2a2a2a для обеих тем ─────
|
||||
// (P1-4: ранее в спецификации был расходящийся набор констант, в т.ч.
|
||||
// ошибочный #1a1a1a для светлой темы тёмнее, чем #2a2a2a для тёмной.
|
||||
// ADR-004 §6 — одна константа #2a2a2a на обе темы.)
|
||||
test('фон под спутником в светлой теме — единая константа #2a2a2a (P1-4)', () => {
|
||||
const env = makeEnv();
|
||||
env.mod.applyBaseLayer('satellite');
|
||||
const bg = env.calls.setPaintProperty.find(
|
||||
(c) => c[0] === 'background' && c[1] === 'background-color'
|
||||
);
|
||||
assert.deepEqual(bg, ['background', 'background-color', '#1a1a1a']);
|
||||
assert.deepEqual(bg, ['background', 'background-color', '#2a2a2a']);
|
||||
});
|
||||
|
||||
test('фон под спутником в тёмной теме — #2a2a2a', () => {
|
||||
test('фон под спутником в тёмной теме — та же константа #2a2a2a (P1-4)', () => {
|
||||
const env = makeEnv({ themeDark: true });
|
||||
env.mod.applyBaseLayer('satellite');
|
||||
const bg = env.calls.setPaintProperty.find(
|
||||
@@ -358,6 +361,60 @@ test('фон под спутником в тёмной теме — #2a2a2a', ()
|
||||
assert.deepEqual(bg, ['background', 'background-color', '#2a2a2a']);
|
||||
});
|
||||
|
||||
test('фон при возврате на «Схему» (light) — baseline #f0ede6 (Data §5)', () => {
|
||||
const env = makeEnv();
|
||||
env.mod.applyBaseLayer('satellite');
|
||||
env.calls.setPaintProperty.length = 0;
|
||||
env.mod.applyBaseLayer('schematic');
|
||||
const bg = env.calls.setPaintProperty.find(
|
||||
(c) => c[0] === 'background' && c[1] === 'background-color'
|
||||
);
|
||||
assert.deepEqual(bg, ['background', 'background-color', '#f0ede6']);
|
||||
});
|
||||
|
||||
test('фон при возврате на «Схему» (dark) — baseline #1a1a2e, не #1a1a1a (P1-4 / P2-3)', () => {
|
||||
const env = makeEnv({ themeDark: true });
|
||||
env.mod.applyBaseLayer('satellite');
|
||||
env.calls.setPaintProperty.length = 0;
|
||||
env.mod.applyBaseLayer('schematic');
|
||||
const bg = env.calls.setPaintProperty.find(
|
||||
(c) => c[0] === 'background' && c[1] === 'background-color'
|
||||
);
|
||||
assert.deepEqual(bg, ['background', 'background-color', '#1a1a2e']);
|
||||
});
|
||||
|
||||
// ── P1-2: POI text-color синхронно с halo ─────────────────────────────
|
||||
test('P1-2: на спутнике poi-labels text-color === #ffffff (читаемо поверх чёрного halo)', () => {
|
||||
const env = makeEnv();
|
||||
env.mod.applyBaseLayer('satellite');
|
||||
const textColor = env.calls.setPaintProperty.find(
|
||||
(c) => c[0] === 'poi-labels' && c[1] === 'text-color'
|
||||
);
|
||||
assert.deepEqual(textColor, ['poi-labels', 'text-color', '#ffffff']);
|
||||
});
|
||||
|
||||
test('P1-2: возврат на «Схему» (light) восстанавливает poi-labels text-color === #333333', () => {
|
||||
const env = makeEnv();
|
||||
env.mod.applyBaseLayer('satellite');
|
||||
env.calls.setPaintProperty.length = 0;
|
||||
env.mod.applyBaseLayer('schematic');
|
||||
const textColor = env.calls.setPaintProperty.find(
|
||||
(c) => c[0] === 'poi-labels' && c[1] === 'text-color'
|
||||
);
|
||||
assert.deepEqual(textColor, ['poi-labels', 'text-color', '#333333']);
|
||||
});
|
||||
|
||||
test('P1-2: возврат на «Схему» (dark) восстанавливает poi-labels text-color === #e0e0e0', () => {
|
||||
const env = makeEnv({ themeDark: true });
|
||||
env.mod.applyBaseLayer('satellite');
|
||||
env.calls.setPaintProperty.length = 0;
|
||||
env.mod.applyBaseLayer('schematic');
|
||||
const textColor = env.calls.setPaintProperty.find(
|
||||
(c) => c[0] === 'poi-labels' && c[1] === 'text-color'
|
||||
);
|
||||
assert.deepEqual(textColor, ['poi-labels', 'text-color', '#e0e0e0']);
|
||||
});
|
||||
|
||||
// ── валидация входа onBaseLayerToggle() ───────────────────────────────
|
||||
test('onBaseLayerToggle() игнорирует некорректное значение', () => {
|
||||
const env = makeEnv();
|
||||
|
||||
Reference in New Issue
Block a user