analyst(ET): auto-commit from analyst run_id=78
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 9s
CI / build (push) Successful in 2s

This commit is contained in:
2026-06-04 09:28:51 +00:00
parent 010b1e72f5
commit 7df1ffe75c
5 changed files with 1796 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
---
type: brd
work_item_id: ET-013
title: "BRD: Сохранить выразительность перепадов высот на z9-z11"
version: 1
status: draft
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
- "agent:analyst"
related:
- "PH-6.terrain"
---
# BRD — ET-013: Сохранить выразительность перепадов высот на z9-z11
## 1. Цель
На зумах **z9-z11** перепады высот должны читаться визуально
сопоставимо с z8: пользователь видит «где холмы, где равнина»,
а не однородную засветку.
Сейчас при увеличении зума с z8 (где перепады бросаются в глаза
через слой «Перепады»/TRI и общий цветовой контраст) до z9-z11
происходит резкая потеря выразительности:
- **z8** — слой «Перепады» (TRI) хорошо читается: крупные пятна
«шершавости» рельефа покрывают значимую долю кадра, базовая
подложка остаётся видна, перепады бросаются в глаза.
- **z9** — кнопка «Тени рельефа» (hillshade) **disabled**
(UI-минзум = 10), TRI ещё работает, но визуально пятна
становятся мельче и контраст слабее.
- **z10-z11** — hillshade включается, но его `opacity=0.40` и
отсутствие усиления контраста делают теневой рельеф «бледной
плёнкой» поверх подложки; TRI не компенсирует, потому что
его `opacity=0.70` рассчитано на z5-z8.
ET-013 = **скалировать paint-параметры (opacity, contrast,
resampling) hillshade и TRI по зуму** так, чтобы на z9-z11
рельеф читался сопоставимо с z8, без перегенерации растровых
тайлов и без новых данных.
## 2. Контекст
### 2.1 Текущая реализация (после PH-6)
**Источники тайлов** (`src/api/main.py:1240`):
- `/terrain/hillshade/{z}/{x}/{y}.png` — теневой рельеф.
- `/terrain/tri/{z}/{x}/{y}.png` — Terrain Ruggedness Index («Перепады»).
- `/terrain/hypso/{z}/{x}/{y}.png` — гипсометрия (на текущий
момент в UI не подключён; вне scope ET-013).
По PH-6 BRD тайлы нарезаны **z8-z14** (PNG 256×256), сгенерированы
из SRTM 30м со следующими параметрами:
- hillshade: azimuth 315°, altitude 45°, **z-factor 1.5**;
- TRI: классификация (flat / nearly flat / slightly rugged /
rugged / very rugged), цветовая шкала.
**Клиентский рендеринг** (`src/web/app.js`):
```js
// Строка ~2782-2783:
applyTerrainLayer('terrain-hillshade', TERRAIN_BASE_URL + '/hillshade/{z}/{x}/{y}.png',
hillshadeChecked, 0.40, 10, 15);
applyTerrainLayer('terrain-tri', TERRAIN_BASE_URL + '/tri/{z}/{x}/{y}.png',
triChecked, 0.70, 5, 15);
```
`applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom)` (строка 3316):
- создаёт `raster` source с `tileSize: 256`, `scheme: 'tms'`,
`minzoom`, `maxzoom`;
- добавляет `raster` layer с paint `{raster-opacity, raster-resampling: 'linear'}`;
- никаких zoom-tier выражений: opacity — **константа**.
**UI-минзум hillshade** (`src/web/app.js:3359`):
```js
function updateHillshadeAvailability() {
const zoom = map.getZoom();
if (zoom < 10) { cb.disabled = true; hint.style.display = 'inline'; ... }
}
```
То есть на z9 чекбокс «Тени рельефа» неактивен и видна подсказка
«Зум 10+». На диске тайл z9 есть (нарезка z8-14), но клиент его
не запрашивает.
### 2.2 Ответы на open questions из бизнес-запроса
| Вопрос | Ответ |
|---|---|
| Чем рисуется рельеф? | Двумя независимыми raster-слоями: **hillshade** (PNG, z8-14 на диске, z10-15 в UI) и **TRI/«Перепады»** (PNG, z8-14 на диске, z5-15 в UI). Гипсометрия в UI сейчас не подключена. |
| Где задаётся стиль по зумам? | `src/web/app.js:2782-2783` (вызовы `applyTerrainLayer` с константой opacity), `src/web/app.js:3316-3357` (создание raster-слоя), `src/web/app.js:3359-3377` (UI-минзум hillshade). Никаких zoom-tier выражений нет — opacity скаляр. |
| До какого зума нарезаны тайлы? | По PH-6 BRD: **z8-z14**. На z15 на клиенте работает overzoom MapLibre (maxzoom source < maxzoom layer). Для ET-013 ключевое: на z9-z11 тайлы **есть на диске** — проблема исключительно в рендеринге. |
| Хватает ли разрешения SRTM 30м на z9-z11? | Да. На z9 1 пиксель тайла ≈ 300м, на z10 ≈ 150м, на z11 ≈ 75м — везде есть запас относительно 30м SRTM. Перепады «теряются» не из-за разрешения данных, а из-за низкого контраста при рендере + отключённого hillshade на z9. |
| Нужен ли отдельный стиль для крупных зумов? | **Нет**, отдельный layer не нужен. Достаточно: (а) снизить UI-минзум hillshade до z9; (б) перевести `raster-opacity` и `raster-contrast` в zoom-aware `interpolate`-выражения; (в) на крупных зумах переключить `raster-resampling` на `nearest`, чтобы перепады были резкими. |
### 2.3 Почему это бизнес-важно
- **UX expectation**: пользователь зумит карту чтобы детальнее
посмотреть рельеф — а получает обратное: «было видно — стало
плоско». Это контр-интуитивно и снижает доверие к слою.
- **Целевая задача продукта** (эндуро-планирование): на z9-z11
пользователь оценивает «насколько холмистая зона между двумя
точками маршрута» — именно этот масштаб ключевой для выбора
направления. Сейчас на этом масштабе слой работает плохо.
- **Низкозатратное исправление**: данные есть, тайлы есть,
логика рендера тривиально дополняется zoom-tier выражениями.
Полезность/стоимость очень высокая.
### 2.4 Что НЕ делаем (обоснование)
| Альтернатива | Решение | Причина |
|---|---|---|
| Перегенерировать hillshade с z-factor 2.5-3.0 для z9-z14 | **Out of scope.** | Требует доступа к infra-pipeline SRTM, пересборки и редеплоя растровых тайлов. Если frontend-калибровки (F-02..F-05) недостаточно — отдельный work item «hillshade-rerender-z9-z14». |
| Добавить векторные горизонтали (contours) | **Out of scope.** | Контуров в стэке нет. Это новая фича уровня PH-6.5, требует pipeline на отдельных vector tiles. |
| Перейти на MapLibre `hillshade` layer (raster-dem) | **Out of scope.** | Требует поднять DEM в формате Terrarium/Mapbox-RGB. Это смена архитектуры рельефа. |
| Multidirectional hillshade (4 азимута) | **Out of scope.** | Требует пересборки тайлов и комбинирования; см. строку 1. |
| Подключить гипсометрию в UI на z9-z11 | **Out of scope.** | Hypso тайлы есть на диске, но UI не имеет переключателя — отдельная задача. |
| Менять PH-6 параметры hillshade (azimuth/altitude) | **Out of scope.** | Это калибровка генератора, не клиентская проблема. |
## 3. Scope
### In scope
| # | Функция |
| ----- | ---------------------------------------------------------------------------------------------------- |
| F-01 | Понизить UI-минзум hillshade с 10 до **9** в `updateHillshadeAvailability` (тайлы z9 есть на диске). |
| F-02 | Понизить `minzoom` источника `terrain-hillshade-source` с 10 до 9 (через изменение вызова `applyTerrainLayer`). |
| F-03 | Опционально: обновить UI-hint «Зум 10+» → «Зум 9+» в `#terrain-hillshade-hint`. |
| F-04 | Расширить `applyTerrainLayer` так, чтобы параметр `opacity` мог быть либо числом (текущий контракт), либо MapLibre `interpolate`-выражением. Никаких новых публичных функций. |
| F-05 | Для hillshade использовать `raster-opacity` zoom-aware: 9→0.65, 10→0.60, 11→0.55, 12→0.50, 14→0.40. Цель: компенсировать «бледность» теней на z9-z11. |
| F-06 | Для hillshade добавить `raster-contrast` zoom-aware: 9→0.40, 10→0.35, 11→0.30, 12→0.15, 14→0.00. Цель: подчеркнуть перепады без перегенерации. |
| F-07 | Для hillshade установить `raster-resampling: 'nearest'` на z9-z11 (т.е. везде, где `raster-resampling` не игнорируется). Цель: резкие края перепадов вместо размытия. Сейчас стоит `'linear'`. Замечание: MapLibre не поддерживает интерполяцию `raster-resampling` по зуму, поэтому компромисс — глобально `'nearest'` для hillshade на всех зумах ≥ 9. На z12+ это допустимо (текстура остаётся читаемой при overzoom). |
| F-08 | Для TRI («Перепады») использовать `raster-opacity` zoom-aware: 5→0.55, 7→0.65, 8→0.70 (как сейчас), 9→0.80, 10→0.85, 11→0.85, 12→0.75, 15→0.70. Цель: усилить TRI ровно на z9-z11 (как компенсацию за рывок hillshade), не трогая z8 и не превращая карту в кашу на z5-z7. |
| F-09 | Для TRI установить `raster-resampling: 'nearest'`. TRI — категориальная классификация (5 уровней), линейный ресемпл размывает границы классов. Цель: резкие границы «спокойно/шероховато». |
| F-10 | UI: контракт переключателей «Тени рельефа» / «Перепады» в `#terrain-popup` не меняется. Чекбоксы, persistence в localStorage (`terrain-hillshade`, `terrain-tri`) — без изменений. |
| F-11 | Регрессия z8: визуально слой «Перепады» на z8 выглядит как раньше (opacity 0.70). |
| F-12 | Регрессия z12-z15: hillshade и TRI не становятся темнее/контрастнее, чем были (calibration возвращается к старым значениям к z14). |
| F-13 | Регрессия performance: количество запросов растровых тайлов на сессию не должно вырасти больше, чем на +35% (грубая оценка: +1 zoom-уровень для hillshade на z9 добавляет ~25% тайлов на сессию активного зумирования). |
| F-14 | Документация: ADR не нужен (это калибровка, не архитектурное решение). Опциональный `06-adr/` остаётся пустым. Изменения покрываются TRZ и комментарием в коде, ссылающимся на ET-013. |
### Out of scope
- **Перегенерация hillshade с большим z-factor** (отдельная задача, см. §2.4).
- **Добавление векторных горизонталей** (отдельная задача).
- **Переход на raster-dem / Mapbox Terrain RGB** (смена архитектуры).
- **Multidirectional hillshade** (требует pipeline).
- **Подключение гипсометрии в UI** (отдельная задача).
- **Изменение PH-6 параметров hillshade на сервере** (azimuth, altitude, z-factor).
- **Изменение генератора TRI** (классификация, цветовая шкала).
- **Тайл-кэш на стороне сервера** (раздача через FastAPI с `Cache-Control: max-age=31536000` уже есть).
- **Изменение UI чекбоксов** (только текст hint'а в F-03).
- **Изменение TERRAIN_DIR / endpoint contract** (`src/api/main.py:1240-1255`).
- **Изменения PWA / offline-кэш стратегии для тайлов** (PH-9, не сейчас).
## 4. Метрики успеха
| # | Метрика | Критерий |
| --- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| M-1 | Hillshade доступен на z9 | Чекбокс «Тени рельефа» при `zoom = 9` **не disabled**; hint скрыт; vector-source запрашивает тайлы при включении. |
| M-2 | Hillshade-opacity zoom-aware | `paint['raster-opacity']` для слоя `terrain-hillshade``interpolate`-выражение со stops для z9, z10, z11, z12, z14. |
| M-3 | Hillshade-contrast zoom-aware | `paint['raster-contrast']``interpolate`-выражение с положительными значениями на z9-z11 и 0 на z14. |
| M-4 | Hillshade-resampling | `paint['raster-resampling']` для `terrain-hillshade` = `'nearest'`. |
| M-5 | TRI-opacity zoom-aware | `paint['raster-opacity']` для `terrain-tri``interpolate`-выражение со stops для z5..z15. |
| M-6 | TRI-resampling | `paint['raster-resampling']` для `terrain-tri` = `'nearest'`. |
| M-7 | Регрессия z8 | На z8 видимость слоя «Перепады» (TRI) визуально не отличается от состояния до ET-013 (opacity stops содержат точку `8 → 0.70`). |
| M-8 | Регрессия z14-z15 | На z14 hillshade visually близок к до-ET-013 (opacity ~0.40, contrast ~0). |
| M-9 | Качественный тест z9-z11 | На скриншоте z10 над холмистым районом (например, юг Москвы / Ока) перепады «явно различимы» — критерий ручной (TC-UI-04-Z10-Q). При отказе — донастройка stops. |
| M-10 | Сетевой объём | При типичной сессии (10 зумов между z8 и z12 c включёнными обоими слоями) объём загруженных PNG-тайлов hillshade и TRI вырос не более чем на 35%. |
## 5. Риски
| # | Риск | Вероятность | Влияние | Митигация |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| R-1 | `raster-contrast` со значением 0.4 даёт «жесть» — пересвет/чернота на тёмных тайлах. | Средняя | Среднее | TC-UI-04-Z10-Q — визуальная приёмка. При проблеме — снизить contrast в stops до 0.25-0.30. F-06 — точки калибруются итеративно. |
| R-2 | На тёмной теме (`theme-dark`, ET-007) hillshade при opacity 0.65 и contrast 0.4 сливается с подложкой в кашу. | Средняя | Среднее | TC-UI-09-Z10-DARK-Q. При проблеме — добавить отдельные stops для dark-theme через `theme-change` event. Прозрачнее (например 0.55 вместо 0.65) на dark. |
| R-3 | На спутниковой подложке (ET-007) opacity 0.65 + contrast 0.4 слишком «глушит» космоснимок. | Низкая | Среднее | TC-UI-08-Z10-SAT-Q. Hillshade на спутнике пользователю обычно не нужен — он использует подложку как замену рельефу. Если визуально некрасиво — на спутнике hillshade оставить opacity 0.40 (старое поведение). |
| R-4 | Снижение UI-минзума hillshade до 9 раздувает сетевой трафик (z9 тайл = 4× больше z8 → область покрывается 4× меньшим числом тайлов, но каждый сессия теперь видит на 1 zoom-уровень больше). | Низкая | Низкое | M-10 (≤ +35%). На практике пользователь либо «включил и не двигается», либо «зумит — тайлы кэшируются». nginx и браузер кэшируют PNG агрессивно (Cache-Control: immutable, см. main.py:1252). |
| R-5 | `raster-resampling: 'nearest'` на overzoom (z12-z15) даёт «пикселизацию», крупные квадраты вместо плавных теней. | Средняя | Низкое | TC-UI-06-Z14-Q. На z12-z14 пользователь обычно отключает hillshade — для города нужна подложка. Если визуально плохо — переключить на `'linear'` на z12+ через JS-логику (отдельный layer). В MVP оставляем `'nearest'`. |
| R-6 | Изменение opacity TRI на z9-z11 (с 0.7 до 0.85) перекрывает грунтовки / тропы (`trails-track`, `trails-path-bridleway`). | Низкая | Низкое | `applyTerrainLayer` уже вставляет terrain-слои **перед** первым слоем `trails-*` или `poi-*` (`src/web/app.js:3337-3339`). z-order остаётся правильным. |
| R-7 | После изменения paint-выражения старый clients (вкладка в браузере) видит «сломанный стиль» при F5. | Очень низкая| Низкое | Простой релоад страницы решает (стили задаются в JS, не в localStorage). Никакой миграции состояния не требуется. |
| R-8 | `interpolate` с `raster-contrast` плохо поддерживается старыми версиями MapLibre. | Низкая | Низкое | MapLibre 4.7.0 (`unpkg.com/maplibre-gl@4.7.0`, см. index.html:10) поддерживает `interpolate` для всех raster paint-properties. |
| R-9 | TRI на z5-z7 при увеличении opacity на крупных зумах остаётся как было — но без stops для z5/z6/z7 может «прыгнуть». | Низкая | Низкое | F-08 явно задаёт stops для z5, z7, z8 — сохранение прежнего поведения на z5-z7. interpolate-линейный гарантирует гладкость. |
| R-10 | Цвета TRI (категориальная палитра) на nearest-resampling показывают резкие границы 30-метровых клеток SRTM — выглядит «зернисто». | Средняя | Низкое | Это и есть желаемое поведение: пользователь видит «реальные» границы перепадов, а не сглаженный туман. Если визуально не нравится — оставить `'linear'` для TRI (откатить F-09). |
| R-11 | Если на test-среде тайлы z9-z11 не нарезаны (расхождение с PH-6 BRD), при включении hillshade на z9 будут 404. | Низкая | Высокое | Pre-implementation check: `curl https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/X/Y.png` должен вернуть 200. Если 404 — задача делится: сначала догенерить тайлы (PH-6 follow-up), потом ET-013. |
## 6. Зависимости
### Frontend
- `src/web/app.js`:
- `onTerrainCheckbox` (~2782): вызовы `applyTerrainLayer`.
- `applyTerrainLayer` (~3316): расширить, чтобы принимать opacity-выражение и paint-объект.
- `updateHillshadeAvailability` (~3359): сменить порог `< 10` на `< 9`.
- `src/web/index.html`:
- `#terrain-hillshade-hint` (строка 60): обновить текст «Зум 10+» → «Зум 9+».
- Стили карты `style.json`/`style-dark.json` — без изменений (растровые слои не описаны в стилях, они добавляются динамически из JS).
### Backend
- `src/api/main.py:1240-1255` (`terrain_tile`) — **без изменений**. Никаких новых endpoint, query, заголовков.
### Тесты
- Новые unit-тесты `tests/unit/test_terrain_paint.py` (новый файл) — проверка структуры paint-выражений (stops, типы значений). Запуск через Node/jsdom либо чистый JS-парсер MapLibre style spec (см. TRZ §3.13).
- Расширение существующих тестов слоёв (если есть). На текущий момент в репо нет тестов для `applyTerrainLayer` — добавляем минимальные.
- UI-тесты: `04b-ui-test-cases.md`.
### Документация
- `01-brd.md` (этот файл).
- `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `04b-ui-test-cases.md`.
- ADR не требуется (это калибровка paint-параметров, не архитектурное решение). Если в реализации возникнет нужда в добавлении dark/satellite-specific paint-таблиц — добавляется `06-adr/adr-0001-theme-specific-terrain.md`.
### Инфра / Данные
- Test-среда `https://openclaw.mva154.duckdns.org/enduro/` — существующий деплой.
- Растровые тайлы рельефа в `/home/slin/enduro-trails/data/terrain/{hillshade,tri}/{z}/{x}/{y}.png`**существующие**, без перегенерации.
- **Обязательная pre-implementation проверка**: тайлы hillshade z9 и z10 над ЦФО действительно доступны (R-11).
```bash
curl -I https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/308/158.png
curl -I https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/10/617/317.png
```
Ожидается HTTP 200 на оба.
### Связи с другими work items
- **PH-6.terrain** — родительская фаза. ET-013 — post-MVP калибровка её UI.
- **ET-007** — переключатель подложки Схема/Спутник. R-3 покрывает совместимость.
- **ET-009 / ET-008** — публичные GPS-треки. Не пересекаются (отдельные источники и слои).
- Будущий work item «hillshade-rerender-z9-z14 с z-factor 2.5» — на случай, если frontend-калибровки недостаточно.
## 7. План в одну строку
Снижаем UI-минзум hillshade с 10 до 9, переводим `raster-opacity` и
`raster-contrast` hillshade в zoom-aware `interpolate`-выражения
с пиком контраста на z9-z11, аналогично усиливаем opacity TRI на
z9-z11, переключаем `raster-resampling` на `'nearest'` — без
перегенерации растровых тайлов и без изменения backend.

View File

@@ -0,0 +1,606 @@
---
type: trz
work_item_id: ET-013
title: "ТЗ: Перепады высот на z9-z11 — zoom-aware paint для hillshade и TRI"
version: 1
status: draft
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
- "agent:analyst"
related:
- "PH-6.terrain"
- "ET-007"
---
# ТЗ — ET-013: Перепады высот на z9-z11
## 1. Терминология
- **Hillshade** — растровый слой теневого рельефа из
`/terrain/hillshade/{z}/{x}/{y}.png`. MapLibre layer id —
`terrain-hillshade`, source id — `terrain-hillshade-source`.
- **TRI** («Перепады») — растровый слой Terrain Ruggedness Index
из `/terrain/tri/{z}/{x}/{y}.png`. Layer id — `terrain-tri`,
source id — `terrain-tri-source`.
- **Zoom-tier paint** — MapLibre `interpolate`-выражение со
stops по `['zoom']`, задаёт значение paint-property как функцию
текущего зума.
- **Raster paint properties** (MapLibre spec):
- `raster-opacity` ∈ [0, 1] — прозрачность слоя.
- `raster-contrast` ∈ [-1, 1] — усиление контраста PNG; 0 — без изменений, > 0 — усиление, < 0 — снижение.
- `raster-resampling``{'linear', 'nearest'}` — алгоритм
масштабирования тайла на пиксели экрана. `'nearest'` даёт
«пиксельные» резкие границы.
- **UI-минзум hillshade** — порог в `updateHillshadeAvailability`,
ниже которого чекбокс «Тени рельефа» disabled. Сейчас 10, после ET-013 — 9.
## 2. Архитектурные опоры
ET-013 не вводит новых слоёв, источников, endpoint'ов. Используем:
- `src/web/app.js`:
- константа `TERRAIN_BASE_URL` (~2726) — без изменений.
- `onTerrainCheckbox` (~2766) — без изменений сигнатуры; меняются
параметры внутри вызовов `applyTerrainLayer`.
- `applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom)` (~3316) —
расширяется (см. REQ-F-04).
- `updateHillshadeAvailability` (~3359) — порог `< 10``< 9`.
- `restoreTerrainState` (~3379) — без изменений (вызывает onTerrainCheckbox).
- `src/web/index.html`:
- `#terrain-hillshade-hint` (строка 60) — текст «Зум 10+» → «Зум 9+».
- `src/api/main.py:1240` (`terrain_tile`) — **без изменений**.
ET-013 = **9 правок: 2 в HTML/text, 7 в одном JS-файле**.
## 3. Требования
### REQ-F-01 — Снизить UI-минзум hillshade до 9
Файл `src/web/app.js`, функция `updateHillshadeAvailability`
(строка ~3368):
```js
if (zoom < 10) {
```
заменить на
```js
if (zoom < 9) { // ET-013: на z9 hillshade уже доступен
```
**Acceptance check.** При `window._map.setZoom(9)` чекбокс
`#terrain-hillshade-cb` имеет `disabled === false` и hint
`#terrain-hillshade-hint` имеет `display: 'none'`.
### REQ-F-02 — Снизить minzoom source `terrain-hillshade-source` до 9
Файл `src/web/app.js`, функция `onTerrainCheckbox` (строка ~2782).
Заменить:
```js
applyTerrainLayer('terrain-hillshade', TERRAIN_BASE_URL + '/hillshade/{z}/{x}/{y}.png',
hillshadeChecked, 0.40, 10, 15);
```
на:
```js
// ET-013: hillshade теперь доступен с z9; opacity и contrast — zoom-aware
applyTerrainLayer('terrain-hillshade',
TERRAIN_BASE_URL + '/hillshade/{z}/{x}/{y}.png',
hillshadeChecked,
HILLSHADE_PAINT, // см. REQ-F-04, REQ-F-05
9, 15);
```
**Acceptance check.** В DevTools после включения слоя:
```js
window._map.getSource('terrain-hillshade-source').minzoom === 9
```
### REQ-F-03 — Снизить minzoom source `terrain-tri-source` остаётся 5
Файл `src/web/app.js`, строка ~2783. Менять только параметр
opacity (см. REQ-F-08). minzoom/maxzoom не трогаем:
```js
applyTerrainLayer('terrain-tri',
TERRAIN_BASE_URL + '/tri/{z}/{x}/{y}.png',
triChecked,
TRI_PAINT, // см. REQ-F-04, REQ-F-08
5, 15);
```
### REQ-F-04 — Расширить `applyTerrainLayer` для поддержки paint-объекта
Файл `src/web/app.js`, функция `applyTerrainLayer` (строки ~3316-3357).
Текущая сигнатура:
```js
function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
...
paint: { 'raster-opacity': opacity, 'raster-resampling': 'linear' },
...
}
```
Новая сигнатура (обратно-совместимая):
```js
/**
* @param {string} id - id слоя.
* @param {string} tileUrl - URL-шаблон тайлов.
* @param {boolean} enabled - показывать ли слой.
* @param {number|object} opacityOrPaint - либо число (старый контракт,
* станет 'raster-opacity'), либо объект paint-properties целиком.
* Если объект — должен содержать как минимум 'raster-opacity'.
* @param {number} minzoom
* @param {number} maxzoom
*/
function applyTerrainLayer(id, tileUrl, enabled, opacityOrPaint, minzoom, maxzoom) {
const map = window._map;
if (!map) return;
const sourceId = id + '-source';
// ET-013: нормализация paint
const paint = (typeof opacityOrPaint === 'number')
? { 'raster-opacity': opacityOrPaint, 'raster-resampling': 'linear' }
: opacityOrPaint;
if (enabled) {
if (!map.getSource(sourceId)) {
map.addSource(sourceId, {
type: 'raster',
tiles: [tileUrl],
tileSize: 256,
scheme: 'tms',
minzoom: minzoom,
maxzoom: maxzoom
});
}
if (!map.getLayer(id)) {
const firstTrailLayer = map.getStyle().layers.find(l =>
l.id.startsWith('trails-') || l.id.startsWith('poi-')
);
map.addLayer({
id: id,
type: 'raster',
source: sourceId,
paint: paint,
minzoom: minzoom,
maxzoom: maxzoom
}, firstTrailLayer ? firstTrailLayer.id : undefined);
}
} else {
if (map.getLayer(id)) map.removeLayer(id);
if (map.getSource(sourceId)) map.removeSource(sourceId);
}
}
```
**Acceptance check.** Unit-тест (см. REQ-F-13):
- `applyTerrainLayer(id, url, true, 0.5, 8, 14)` — старый контракт работает.
- `applyTerrainLayer(id, url, true, {'raster-opacity': 0.5, 'raster-contrast': 0.3, 'raster-resampling': 'nearest'}, 8, 14)` — paint применён как есть.
### REQ-F-05 — Hillshade `raster-opacity` zoom-aware
Файл `src/web/app.js`, после определения `TERRAIN_BASE_URL` (после строки ~2726)
добавить блок констант:
```js
// ET-013: zoom-aware paint для слоёв рельефа.
// Цель — компенсировать «потерю выразительности» перепадов на z9-z11.
// Pre-z9 — hillshade не показывается (UI-минзум). На z9-z11 — максимальный
// контраст и opacity, чтобы тени читались как на z8. К z12-z14 — возврат
// к исходным значениям (тогда у пользователя есть другие способы
// читать рельеф: подложка, грунтовки, POI).
const HILLSHADE_PAINT = {
'raster-opacity': [
'interpolate', ['linear'], ['zoom'],
9, 0.65,
10, 0.60,
11, 0.55,
12, 0.50,
14, 0.40
],
'raster-contrast': [
'interpolate', ['linear'], ['zoom'],
9, 0.40,
10, 0.35,
11, 0.30,
12, 0.15,
14, 0.00
],
'raster-resampling': 'nearest'
};
```
Stops подобраны так:
- z9-z11 — пик opacity (0.65→0.55) и contrast (0.40→0.30). Это
компенсация: тени темнее и контрастнее.
- z12-z14 — плавный возврат к исходному (opacity 0.40, contrast 0):
на крупных зумах пользователь уже видит подложку детально и
тени должны «уйти на второй план».
- `'nearest'` resampling: подчёркивает 30-метровые границы SRTM,
перепады выглядят резко.
**Acceptance check.**
```js
const layer = window._map.getLayer('terrain-hillshade');
const opacity = window._map.getPaintProperty('terrain-hillshade', 'raster-opacity');
Array.isArray(opacity) && opacity[0] === 'interpolate' // true
```
### REQ-F-06 — Hillshade `raster-contrast` (внутри HILLSHADE_PAINT)
См. REQ-F-05. Constants выносятся в HILLSHADE_PAINT, отдельной правки кода не нужно.
### REQ-F-07 — Hillshade `raster-resampling: 'nearest'`
См. REQ-F-05. Часть HILLSHADE_PAINT.
### REQ-F-08 — TRI `raster-opacity` zoom-aware
В том же блоке (после HILLSHADE_PAINT, до `function toggleTerrainPopup`):
```js
const TRI_PAINT = {
'raster-opacity': [
'interpolate', ['linear'], ['zoom'],
5, 0.55,
7, 0.65,
8, 0.70, // регрессия z8: текущее значение
9, 0.80,
10, 0.85,
11, 0.85, // пик на z9-z11
12, 0.75,
15, 0.70
],
'raster-resampling': 'nearest'
};
```
Stops:
- **z5-z7** — мягко (0.55-0.65), на «обзорных» зумах не глушим карту.
- **z8** — 0.70 ровно как сейчас (регрессия).
- **z9-z11** — пик 0.80-0.85 (целевое улучшение ET-013).
- **z12-z15** — спад до 0.70-0.75.
**Acceptance check.**
```js
const opacity = window._map.getPaintProperty('terrain-tri', 'raster-opacity');
// На z8 — 0.70 ровно (регрессия).
// На z10 — 0.85 ровно (целевое поведение).
```
### REQ-F-09 — TRI `raster-resampling: 'nearest'`
Часть TRI_PAINT, см. REQ-F-08.
### REQ-F-10 — Обновить UI-hint текст
Файл `src/web/index.html`, строка ~60:
```html
<span class="terrain-hint" id="terrain-hillshade-hint" style="display:none">Зум 10+</span>
```
заменить на
```html
<span class="terrain-hint" id="terrain-hillshade-hint" style="display:none">Зум 9+</span>
```
### REQ-F-11 — `updateHillshadeAvailability` использует новый порог
См. REQ-F-01. Никаких других изменений в этой функции не нужно.
### REQ-F-12 — Сохранить контракт `onTerrainCheckbox`
Сигнатура и логика persistence в `localStorage` (`terrain-hillshade`,
`terrain-tri`) — без изменений. Кнопка `#terrain-toggle` `.active`
переключается так же.
### REQ-F-13 — Unit-тесты paint-выражений
Файл `tests/unit/test_terrain_paint.js` (новый; если JS-тесты раньше
не было — настроить vitest/jest в `package.json` либо использовать
существующий тест-раннер; альтернатива — Python-парсер JSON-выражений).
Реализация в одной из двух форм:
**Вариант A: JS unit-тест (jest/vitest)**
```js
// tests/unit/test_terrain_paint.test.js
import { HILLSHADE_PAINT, TRI_PAINT } from '../../src/web/terrain-paint.js';
// Если константы внутри app.js: либо вынести в отдельный модуль,
// либо использовать AST-парсер. См. альтернативу B.
describe('ET-013 terrain paint', () => {
test('HILLSHADE_PAINT: raster-opacity is interpolate by zoom', () => {
const op = HILLSHADE_PAINT['raster-opacity'];
expect(op[0]).toBe('interpolate');
expect(op[1][0]).toBe('linear');
expect(op[2][0]).toBe('zoom');
// stops: ..., 9, 0.65, 10, 0.60, 11, 0.55, 12, 0.50, 14, 0.40
const stops = op.slice(3);
expect(stops).toContain(9);
expect(stops[stops.indexOf(9) + 1]).toBeCloseTo(0.65, 2);
expect(stops[stops.indexOf(11) + 1]).toBeCloseTo(0.55, 2);
expect(stops[stops.indexOf(14) + 1]).toBeCloseTo(0.40, 2);
});
test('HILLSHADE_PAINT: raster-contrast peak at z9-z11', () => {
const c = HILLSHADE_PAINT['raster-contrast'];
expect(c[0]).toBe('interpolate');
const stops = c.slice(3);
expect(stops[stops.indexOf(9) + 1]).toBeGreaterThanOrEqual(0.35);
expect(stops[stops.indexOf(14) + 1]).toBeLessThanOrEqual(0.05);
});
test('HILLSHADE_PAINT: resampling nearest', () => {
expect(HILLSHADE_PAINT['raster-resampling']).toBe('nearest');
});
test('TRI_PAINT: z8 unchanged (regression)', () => {
const op = TRI_PAINT['raster-opacity'];
const stops = op.slice(3);
expect(stops[stops.indexOf(8) + 1]).toBeCloseTo(0.70, 2);
});
test('TRI_PAINT: peak at z9-z11', () => {
const op = TRI_PAINT['raster-opacity'];
const stops = op.slice(3);
expect(stops[stops.indexOf(10) + 1]).toBeGreaterThanOrEqual(0.80);
expect(stops[stops.indexOf(11) + 1]).toBeGreaterThanOrEqual(0.80);
});
test('TRI_PAINT: resampling nearest', () => {
expect(TRI_PAINT['raster-resampling']).toBe('nearest');
});
});
```
**Вариант B: Python-парсер (если JS-тестов в проекте нет)**
```python
# tests/unit/test_terrain_paint.py
import re
from pathlib import Path
APP_JS = Path(__file__).parents[2] / 'src/web/app.js'
def test_hillshade_paint_exists():
txt = APP_JS.read_text(encoding='utf-8')
assert 'HILLSHADE_PAINT' in txt
assert "'raster-opacity'" in txt
assert "'raster-contrast'" in txt
assert "'raster-resampling': 'nearest'" in txt
def test_hillshade_opacity_stops():
"""Сверяем stops по grep — недостаточно строго, но удержит регрессию."""
txt = APP_JS.read_text(encoding='utf-8')
# ищем блок HILLSHADE_PAINT и проверяем stop'ы
m = re.search(r"HILLSHADE_PAINT\s*=\s*\{(.+?)\};", txt, re.DOTALL)
assert m, "HILLSHADE_PAINT not found"
block = m.group(1)
assert '9, 0.65' in block or '9, 0.65' in block
assert '11, 0.55' in block
assert '14, 0.40' in block
def test_tri_opacity_regression_z8():
txt = APP_JS.read_text(encoding='utf-8')
m = re.search(r"TRI_PAINT\s*=\s*\{(.+?)\};", txt, re.DOTALL)
assert m
block = m.group(1)
assert '8, 0.70' in block or '8, 0.70' in block, "z8 opacity должна остаться 0.70"
assert '10, 0.85' in block
```
**Решение по умолчанию для ET-013:** Вариант B (Python-парсер),
т.к. в проекте JS-тестов не существует, а ставить vitest ради ET-013
— превышение scope. Опционально разработчик может выбрать Вариант A.
### REQ-F-14 — Регрессионные тесты
Файл `tests/unit/test_terrain_paint.py` (тот же файл, что и REQ-F-13):
- **UT-REG-01.** Проверить, что вызов `applyTerrainLayer` с числовым
`opacity` (старый контракт) собирает paint `{raster-opacity: X, raster-resampling: 'linear'}`
на случай, если другой код (POI, halo, scenic) использует ту же
функцию. На текущий момент `applyTerrainLayer` вызывается **только**
внутри `onTerrainCheckbox` — но контракт должен оставаться обратно-совместимым.
Реализация — статический grep по `src/web/`:
```python
import re, glob
def test_only_two_callers_of_applyterrainLayer():
pattern = re.compile(r'applyTerrainLayer\s*\(')
total = 0
for f in glob.glob('src/web/*.js'):
total += len(pattern.findall(open(f).read()))
assert total >= 2 # минимум 2 вызова в onTerrainCheckbox
```
- **UT-REG-02.** `updateHillshadeAvailability` порог = 9
(grep по строке `zoom < 9`).
### REQ-F-15 — Integration smoke-тест: тайлы z9 доступны
Файл `tests/integration/test_terrain_z9_tiles.py` (новый):
- **IT-TILE-Z9-01.** При наличии `data/terrain/hillshade/9/`
директории — запрос `GET /terrain/hillshade/9/308/158.png`
возвращает 200, content-type `image/png`. Если директория
не существует — тест **skipped** с пояснением.
```python
import os, pytest
from fastapi.testclient import TestClient
from src.api.main import app
TERRAIN_DIR = os.environ.get(
'TERRAIN_DIR', os.path.join(os.path.dirname(__file__), '../../data/terrain')
)
client = TestClient(app)
@pytest.mark.skipif(
not os.path.isdir(os.path.join(TERRAIN_DIR, 'hillshade/9')),
reason='hillshade z9 tiles not present in CI (PH-6 data not in repo)'
)
def test_hillshade_z9_tile_returns_200():
# Любой существующий тайл из директории
z9_dir = os.path.join(TERRAIN_DIR, 'hillshade/9')
x = sorted(os.listdir(z9_dir))[0]
y_file = sorted(os.listdir(os.path.join(z9_dir, x)))[0]
y = y_file.replace('.png', '')
r = client.get(f'/terrain/hillshade/9/{x}/{y}.png')
assert r.status_code == 200
assert r.headers['content-type'] == 'image/png'
def test_hillshade_invalid_zoom_404():
r = client.get('/terrain/hillshade/99/0/0.png')
assert r.status_code == 404
```
### REQ-F-16 — UI-тесты Playwright
См. `04b-ui-test-cases.md`. Ключевые проверки (полный список — там):
- TC-UI-01-Z9: hillshade доступен на z9, hint скрыт.
- TC-UI-02-Z8-REGR: на z8 TRI визуально как до ET-013.
- TC-UI-03-Z9-Q: визуальная читаемость перепадов на z9 ≥ z8 (качественно).
- TC-UI-04-Z10-Q: то же для z10.
- TC-UI-05-Z11-Q: то же для z11.
- TC-UI-06-Z14-Q: на z14 hillshade «нормальный», не перегретый.
- TC-UI-07-Z9-MOBILE: мобильный viewport, hillshade видим на z9.
- TC-UI-08-Z10-SAT-Q: совместимость со спутниковой подложкой.
- TC-UI-09-Z10-DARK-Q: совместимость с тёмной темой.
- TC-UI-10-PERSIST: localStorage `terrain-hillshade`/`terrain-tri`
переживает перезагрузку, паттерн чекбоксов восстанавливается.
### REQ-F-17 — Persistence без миграции
Ключи `localStorage`:
- `terrain-hillshade` ('1' | '0') — без изменений.
- `terrain-tri` ('1' | '0') — без изменений.
После ET-013 пользователи с включённым hillshade при следующей
загрузке на z9 увидят слой автоматически (раньше он был disabled).
Это не миграция, а ожидаемое улучшение UX.
### REQ-F-18 — Не менять API контракт
`GET /terrain/{layer}/{z}/{x}/{y}.png` — без изменений. Никаких
новых query, headers, кодов ответа. `Cache-Control: immutable`
сохраняется.
### REQ-F-19 — Не менять конфиги и стили
- `src/web/style.json`, `src/web/style-dark.json` — без изменений.
- `src/web/app.css` — без изменений (стили чекбоксов не меняются).
- `config/*.yaml` — без изменений.
### REQ-F-20 — Деплой и валидация
После merge в `main` и деплоя:
1. **Pre-merge sanity** (на test-среде до деплоя):
```bash
curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/308/158.png | head -1
```
Ожидается `HTTP/1.1 200 OK`. Если 404 — задача останавливается,
тайлы z9 нужно догенерировать в рамках PH-6 follow-up.
2. **Smoke в test-среде**:
- Открыть карту, центр над Окой/югом Москвы (`[37.6, 54.5]`).
- `window._map.setZoom(9)` — кнопка «Тени рельефа» активна.
- Включить «Тени рельефа» и «Перепады».
- Скриншот → визуальная приёмка по AC-03..AC-05.
3. **Зафиксировать в `14-deploy-log.md`**.
### REQ-F-21 — Документация
В `docs/work-items/ET-013/` после Анализа:
- `00-business-request.md` (есть)
- `01-brd.md`
- `02-trz.md` (этот файл)
- `03-acceptance-criteria.md`
- `04-test-plan.yaml`
- `04b-ui-test-cases.md`
После реализации: `12-review.md`, `13-test-report.md`,
`14-deploy-log.md`. ADR опционально (см. BRD §6).
## 4. Не-функциональные требования
### NFR-01 — Производительность клиента
- Добавление двух `interpolate`-выражений в paint не должно
заметно увеличивать render time. MapLibre кэширует
скомпилированные style-выражения; разница < 1 мс на frame.
- `raster-resampling: 'nearest'` дешевле, чем `'linear'`
(без bilinear-фильтрации) — на самом деле небольшое
ускорение растеризации.
### NFR-02 — Производительность сервера
Без изменений: endpoint `terrain_tile` отдаёт PNG из файловой системы
с `Cache-Control: immutable`.
### NFR-03 — Сетевой трафик
- При снижении UI-минзума hillshade с 10 до 9 пользователь
может видеть слой на одной zoom-ступени раньше, что добавляет
~25-35% PNG-тайлов на типичную сессию активного зумирования
с включённым hillshade.
- Browser-кэш + nginx-кэш (`Cache-Control: max-age=31536000,
immutable`) поглощают это после первого визита.
- Регрессия `M-10`: рост ≤ 35%.
### NFR-04 — Совместимость
- MapLibre 4.7.0 (см. `index.html:10`, `index.html:503`)
поддерживает все используемые paint properties и
`interpolate`-выражения.
- Старые tab'ы (без обновления страницы) продолжают работать
с прежним кодом до перезагрузки.
### NFR-05 — Безопасность
Никаких изменений в auth / CSP / валидации.
### NFR-06 — Логирование
Никаких новых лог-сообщений. `uvicorn.access` для `/terrain/*`
работает как раньше.
### NFR-07 — Persistence
`localStorage` — без миграции. Существующие ключи интерпретируются
как раньше; включённый ранее hillshade автоматически появится на
z9 при следующей загрузке.
## 5. План работ (для разработчика)
1. **Pre-implementation check**: проверить наличие тайлов z9-z11
на test-среде (REQ-F-20 §1). Если 404 — стоп, открыть PH-6
follow-up.
2. **Frontend constants**: добавить `HILLSHADE_PAINT` и `TRI_PAINT`
(REQ-F-05, F-08) после `TERRAIN_BASE_URL`.
3. **Frontend `applyTerrainLayer`**: расширить сигнатуру (REQ-F-04).
4. **Frontend `onTerrainCheckbox`**: перевести вызовы на константы
(REQ-F-02, F-03).
5. **Frontend `updateHillshadeAvailability`**: порог `< 10` → `< 9`
(REQ-F-01, F-11).
6. **HTML hint**: «Зум 10+» → «Зум 9+» (REQ-F-10).
7. **Тесты**: `tests/unit/test_terrain_paint.py` (REQ-F-13, F-14).
8. **Integration smoke**: `tests/integration/test_terrain_z9_tiles.py`
(REQ-F-15) — с `@pytest.mark.skipif` для CI без данных.
9. **`make lint` / `make test`** — должны пройти.
10. **Code review → merge → deploy в test**.
11. **Ручная валидация** (REQ-F-20 §2).
12. **Playwright UI-тесты** по `04b-ui-test-cases.md`.
13. **Запись в `13-test-report.md` и `14-deploy-log.md`**.
## 6. Открытые вопросы и решения по умолчанию
| Вопрос | Решение по умолчанию |
|---|---|
| Стоит ли понижать UI-минзум hillshade ещё дальше (z8)? | **Нет.** На z8 hillshade-тайлы 256px покрывают ~150 км по широте — крупные тени становятся неразборчивым «шумом». TRI работает лучше. Если будущий BRD захочет — отдельная задача. |
| Стоит ли использовать разные paint для тёмной темы (`theme-dark`)? | **Не в MVP.** Если AC-09 (TC-UI-09-Z10-DARK-Q) показывает «слой сливается с подложкой» — добавить ADR-0001 о theme-specific paint в follow-up. |
| Стоит ли использовать разные paint для спутниковой подложки? | **Не в MVP.** Hillshade на спутнике пользователю обычно не нужен — он использует подложку как замену рельефу. Если AC-08 (TC-UI-08-Z10-SAT-Q) показывает «глушит подложку» — отдельная итерация. |
| Стоит ли добавить `raster-saturation` для TRI? | **Не в MVP.** Сначала смотрим на эффект от `raster-opacity` + `'nearest'`. Если визуально недостаточно ярко — добавить второй раунд калибровки. |
| Перегенерировать ли hillshade с z-factor 2.5 для z9-z14? | **Не сейчас.** Отдельная задача в случае, если frontend-калибровка ET-013 не решает проблему (вероятность по моей оценке — низкая). |
| Менять ли `raster-resampling` динамически по зуму? | **Нет.** MapLibre не поддерживает `interpolate` для `raster-resampling`. Глобальное `'nearest'` для обоих слоёв — приемлемый компромисс (см. R-5). |
| Подключить ли гипсометрию в UI? | **Out of scope.** Hypso тайлы есть, но UI-чекбокса нет. Отдельная задача. |
| Делать ли paint-таблицы переменными окружения / config'ом? | **Нет.** Это калибровка, она живёт в коде и меняется коммитом. Конфигурируемость — преждевременная абстракция. |
| Стоит ли добавлять `vitest`/`jest` ради JS-unit-тестов? | **Нет в ET-013.** Используем Python-парсер (Вариант B в REQ-F-13). |

View File

@@ -0,0 +1,236 @@
---
type: acceptance-criteria
work_item_id: ET-013
title: "Acceptance Criteria: Перепады высот на z9-z11"
version: 1
status: draft
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
- "agent:analyst"
---
# Acceptance Criteria — ET-013
Критерии в Gherkin-стиле. Все обязательные. Задача считается
принятой, когда каждый критерий прошёл проверку (автоматическую
в CI или ручную в test-среде).
## AC-01 — UI-минзум hillshade понижен до 9
**Given** ветка `feature/ET-013-z9-z11-z8` после реализации
**When** проверяется код
**Then**:
- В `src/web/app.js` функция `updateHillshadeAvailability` содержит
`if (zoom < 9)` (а не `< 10`).
- В `src/web/index.html` элемент `#terrain-hillshade-hint` содержит
текст «Зум 9+» (а не «Зум 10+»).
## AC-02 — Vector-source `terrain-hillshade-source` имеет minzoom=9
**Given** test-среда после деплоя ET-013, включены оба чекбокса слоёв рельефа
**When** в DevTools выполнить
```js
window._map.getSource('terrain-hillshade-source').minzoom
```
**Then** результат — `9`.
## AC-03 — При z=9 hillshade доступен и виден
**Given** пользователь на test-среде, центр карты над холмистым
районом (например, юг Москвы / Ока: `[37.6, 54.5]`)
**When** установить `window._map.setZoom(9)`, открыть `#terrain-popup`,
включить «Тени рельефа»
**Then**:
- `#terrain-hillshade-cb` имеет `disabled === false`.
- `#terrain-hillshade-hint` имеет `display: 'none'`.
- `window._map.getLayoutProperty('terrain-hillshade', 'visibility') === 'visible'`.
- На карте видны тени рельефа.
## AC-04 — Hillshade paint zoom-aware
**Given** включён hillshade на test-среде
**When** в DevTools выполнить
```js
const op = window._map.getPaintProperty('terrain-hillshade', 'raster-opacity');
const ct = window._map.getPaintProperty('terrain-hillshade', 'raster-contrast');
const rs = window._map.getPaintProperty('terrain-hillshade', 'raster-resampling');
```
**Then**:
- `Array.isArray(op) && op[0] === 'interpolate'` (zoom-aware opacity).
- `Array.isArray(ct) && ct[0] === 'interpolate'` (zoom-aware contrast).
- `rs === 'nearest'`.
## AC-05 — TRI paint zoom-aware
**Given** включён TRI на test-среде
**When** в DevTools
```js
const op = window._map.getPaintProperty('terrain-tri', 'raster-opacity');
const rs = window._map.getPaintProperty('terrain-tri', 'raster-resampling');
```
**Then**:
- `Array.isArray(op) && op[0] === 'interpolate'`.
- На z=8 эффективное значение `≈ 0.70` (регрессия).
- На z=10 эффективное значение `≥ 0.80`.
- `rs === 'nearest'`.
## AC-06 — Регрессия z8: TRI визуально как было
**Given** test-среда после деплоя
**When** установить `zoom = 8`, включить ТОЛЬКО «Перепады» (без hillshade)
**Then**:
- Скриншот `et013-z8-tri-regress.png` не отличается визуально
заметно от состояния до ET-013 (сравнение оператором).
- Hillshade-слой не присутствует в стиле (`!map.getLayer('terrain-hillshade')`).
## AC-07 — Качественная читаемость z9 (целевой критерий)
**Given** test-среда, центр над Окой / Кашира / Воробьёвы Горы
**When** `zoom = 9`, включены оба слоя «Тени рельефа» и «Перепады»
**Then**:
- На скриншоте `et013-z9-readable.png` явно видны перепады
высот: тени по склонам, цветные пятна TRI выделяют шероховатые
зоны.
- Оператор подтверждает: «перепады сопоставимы с z8 или лучше».
- При отказе — корректировка stops в HILLSHADE_PAINT / TRI_PAINT.
## AC-08 — Качественная читаемость z10
**Given** test-среда, аналогично AC-07
**When** `zoom = 10`
**Then**: то же, что AC-07.
## AC-09 — Качественная читаемость z11
**Given** test-среда, аналогично AC-07
**When** `zoom = 11`
**Then**: то же, что AC-07.
## AC-10 — Регрессия z14: hillshade не перегрет
**Given** test-среда
**When** `zoom = 14`, включён hillshade
**Then**:
- Эффективные значения `raster-opacity ≈ 0.40`, `raster-contrast ≈ 0`.
- Скриншот `et013-z14-regress.png` не темнее и не контрастнее, чем
до ET-013.
## AC-11 — Hillshade на тёмной теме читается
**Given** test-среда, `theme-dark` активна
**When** `zoom = 10`, включён hillshade
**Then**:
- Тени видны, не сливаются с тёмной подложкой.
- При отказе (тени «съедают» карту) — открыть ADR
«theme-specific hillshade paint» и добавить отдельные stops
для dark-theme (см. BRD R-2). В рамках MVP ET-013 это
не обязательно, но фиксируется в `13-test-report.md`.
## AC-12 — Hillshade на спутниковой подложке не глушит снимок
**Given** test-среда, переключена подложка `#base-btn-satellite`
**When** `zoom = 10`, включён hillshade
**Then**:
- На спутниковом снимке видны и детали поверхности (рельеф
улавливается уже через тени снимка), и hillshade-оверлей.
- Оверлей не превращает снимок в «серую плёнку».
- Подтверждается оператором по TC-UI-08-Z10-SAT-Q.
## AC-13 — Hillshade на мобильном (375×667)
**Given** Playwright mobile viewport, включён hillshade
**When** `zoom = 9`
**Then**:
- Тени видны, читаемы.
- Чекбоксы и hint работают корректно.
## AC-14 — Persistence не сломан
**Given** включены оба чекбокса
**When** перезагрузить страницу (`location.reload()`)
**Then**:
- `localStorage.getItem('terrain-hillshade') === '1'`.
- `localStorage.getItem('terrain-tri') === '1'`.
- После загрузки слои восстановлены, на z=9 hillshade автоматически
активен.
## AC-15 — Unit-тесты paint-выражений зелёные
**Given** ветка
**When** `pytest tests/unit/test_terrain_paint.py -v`
**Then** все тесты проходят (UT-PAINT-*, UT-REG-*).
## AC-16 — Integration smoke z9 тайлов
**Given** ветка, наличие данных в test-среде или CI fixture
**When** `pytest tests/integration/test_terrain_z9_tiles.py -v`
**Then**:
- При наличии тайлов `data/terrain/hillshade/9/*` — тесты
проходят: 200 на существующий тайл, 404 на невалидный zoom.
- При отсутствии тайлов в CI — тесты `skipped` с reason.
## AC-17 — Регрессионные тесты ET-007 / PH-6
**Given** ветка
**When** `pytest tests/unit/ tests/integration/ -v`
**Then**:
- Все существующие тесты ET-007 (переключатель Схема/Спутник)
и PH-6 проходят без регрессий.
- Никакие тесты grandfather'ов не отвалились.
## AC-18 — `make lint` и `make test` зелёные
**Given** ветка
**When** `make lint && make test`
**Then** exit-code 0 на обе команды.
## AC-19 — Pre-deploy проверка наличия тайлов z9-z11
**Given** ветка готова к merge
**When** на test-среде
```bash
curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/308/158.png
curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/10/617/317.png
curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/11/1234/635.png
```
**Then** все три запроса возвращают HTTP 200. Если 404 на любой —
merge приостанавливается, открывается PH-6 follow-up (догенерить
тайлы).
## AC-20 — Документация полная
**Given** репо после слияния ET-013
**When** проверка `docs/work-items/ET-013/`
**Then** существуют:
- `00-business-request.md`
- `01-brd.md`
- `02-trz.md`
- `03-acceptance-criteria.md`
- `04-test-plan.yaml`
- `04b-ui-test-cases.md`
- `12-review.md` (после Review)
- `13-test-report.md` (после Тестирования)
- `14-deploy-log.md` (после Деплоя)
## AC-21 — Сетевая регрессия (M-10)
**Given** test-среда
**When** сценарий: открыть карту, центр над Окой, выполнить
zoom-последовательность z=8 → z=9 → z=10 → z=11 → z=10 → z=9 → z=8
с включёнными обоими слоями
**Then**:
- Суммарный network-traffic PNG-тайлов рельефа ≤ 135% от того же
сценария до ET-013 (зафиксированного как baseline в
`13-test-report.md`).
- Никаких сторонних запросов (например, 4xx или 5xx) не возникает.
## AC-22 — Контракт `applyTerrainLayer` обратно-совместим
**Given** ветка
**When** unit-тест UT-PAINT-COMPAT-01
**Then**:
- Вызов `applyTerrainLayer(id, url, true, 0.5, 8, 14)`
(старый контракт — число) собирает paint:
`{ 'raster-opacity': 0.5, 'raster-resampling': 'linear' }`.
- Вызов с object'ом передаёт paint как есть.

View File

@@ -0,0 +1,336 @@
---
type: test-plan
work_item_id: ET-013
title: "Test Plan: Перепады высот на z9-z11"
version: 1
status: draft
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
- "agent:analyst"
related:
- "PH-6.terrain"
- "ET-007"
scope_note: >
ET-013 — frontend-калибровка: понижает UI-минзум hillshade с 10 до 9
и переводит paint-параметры (raster-opacity, raster-contrast,
raster-resampling) hillshade и TRI в zoom-aware форму. Backend
и pipeline растровых тайлов не трогаются. Тест-план фокусируется
на:
(1) корректности новых zoom-tier paint-выражений;
(2) обратной совместимости applyTerrainLayer;
(3) визуальной читаемости перепадов на z9-z11;
(4) регрессии z8 (TRI не изменился), z14 (hillshade не перегрет);
(5) совместимости с тёмной темой и спутниковой подложкой;
(6) что network-объём не уплыл больше +35%.
test_suites:
- name: unit-terrain-paint
type: unit
description: "Структура paint-выражений HILLSHADE_PAINT и TRI_PAINT"
cases:
- id: UT-PAINT-HS-OPACITY
name: "HILLSHADE_PAINT: raster-opacity — interpolate с правильными stops"
input: |
Python-парсер: чтение src/web/app.js, regex по блоку
HILLSHADE_PAINT = { ... }; вытаскивание raster-opacity.
expected: |
Тип: ['interpolate', ['linear'], ['zoom'], ...].
Stops содержат: (9, 0.65), (10, 0.60), (11, 0.55),
(12, 0.50), (14, 0.40). Допустимо отклонение значений ±0.05
(калибровка) — но порядок монотонно убывающий от 9 к 14.
- id: UT-PAINT-HS-CONTRAST
name: "HILLSHADE_PAINT: raster-contrast — пик на z9, 0 на z14"
input: |
Тот же парсер.
expected: |
Тип interpolate. Значение на z=9 ≥ 0.30. Значение на z=14
≤ 0.10. Монотонно убывает.
- id: UT-PAINT-HS-RESAMPLING
name: "HILLSHADE_PAINT: raster-resampling = 'nearest'"
input: |
Парсер.
expected: |
Строка 'nearest' (не 'linear').
- id: UT-PAINT-TRI-OPACITY-Z8
name: "TRI_PAINT: на z8 opacity = 0.70 (регрессия)"
input: |
Парсер по TRI_PAINT.
expected: |
Stop (8, 0.70) присутствует ровно (без округления).
- id: UT-PAINT-TRI-OPACITY-PEAK
name: "TRI_PAINT: пик на z9-z11"
input: |
Парсер.
expected: |
Stops содержат (10, X) с X ≥ 0.80 и (11, Y) с Y ≥ 0.80.
- id: UT-PAINT-TRI-RESAMPLING
name: "TRI_PAINT: raster-resampling = 'nearest'"
input: |
Парсер.
expected: |
'nearest'.
- id: UT-PAINT-COMPAT-01
name: "applyTerrainLayer обратно-совместим с числовым opacity"
input: |
Вызов с opacity=0.5 (Node + JSDOM-mock карты).
expected: |
Внутри map.addLayer передан paint:
{ 'raster-opacity': 0.5, 'raster-resampling': 'linear' }.
notes: |
Если запуск JS-теста не настроен — заменить на статический
grep по src/web/app.js: проверить ветвление
'typeof opacityOrPaint === "number"'.
- id: UT-PAINT-COMPAT-02
name: "applyTerrainLayer принимает paint-объект"
input: |
Вызов с opacityOrPaint = { 'raster-opacity': 0.4,
'raster-contrast': 0.2, 'raster-resampling': 'nearest' }.
expected: |
Этот объект передан в map.addLayer paint как есть.
- id: UT-REG-MINZOOM-9
name: "updateHillshadeAvailability порог = 9"
input: |
grep по src/web/app.js: 'if (zoom < 9)' внутри функции
updateHillshadeAvailability.
expected: |
Совпадение найдено; 'if (zoom < 10)' отсутствует.
- id: UT-REG-HINT-TEXT
name: "Hint текст обновлён до 'Зум 9+'"
input: |
grep по src/web/index.html: '#terrain-hillshade-hint'
содержит 'Зум 9+'.
expected: |
Совпадение найдено; 'Зум 10+' отсутствует.
- id: UT-REG-CALLERS
name: "applyTerrainLayer вызывается ровно дважды в onTerrainCheckbox"
input: |
regex 'applyTerrainLayer\s*\(' в src/web/*.js — count.
expected: |
Минимум 2 вызова в src/web/app.js. Все они находятся
внутри функции onTerrainCheckbox.
- name: integration-terrain-tiles
type: integration
description: "Endpoint /terrain/{layer}/{z}/{x}/{y}.png на z9-z11"
cases:
- id: IT-TILE-Z9-01
name: "Тайл z=9 для hillshade: 200 или skipped если данных нет"
input: |
Test-среда или CI с TERRAIN_DIR. Найти первый существующий
тайл z9 в директории hillshade, выполнить GET.
expected: |
Если data/terrain/hillshade/9/ существует:
status 200, content-type image/png, тело > 0.
Иначе:
test skipped с reason 'PH-6 data not in repo'.
- id: IT-TILE-Z10-01
name: "Тайл z=10 для hillshade: 200 или skipped"
input: |
То же, что IT-TILE-Z9-01 для z=10.
expected: |
status 200 или skipped.
- id: IT-TILE-Z11-01
name: "Тайл z=11 для hillshade: 200 или skipped"
input: |
То же для z=11.
expected: |
status 200 или skipped.
- id: IT-TILE-TRI-Z9
name: "TRI на z9 доступен (минзум 5, тайлы должны быть)"
input: |
GET tiles/9/X/Y.png под TRI.
expected: |
200 или skipped (если данных нет на CI).
- id: IT-TILE-INVALID-LAYER
name: "Неизвестный layer → 404 (регрессия)"
input: |
GET /terrain/unknown/9/0/0.png
expected: |
status 404.
- id: IT-TILE-MISSING
name: "Несуществующий тайл → 404 (регрессия)"
input: |
GET /terrain/hillshade/9/99999/99999.png
expected: |
status 404.
- id: IT-TILE-CACHE-HEADER
name: "Cache-Control: immutable сохраняется"
input: |
GET существующего тайла.
expected: |
Header 'Cache-Control' содержит 'immutable' и max-age=31536000.
- name: regression-existing
type: regression
description: "Регрессия ET-007 / PH-6 / общих unit-тестов"
cases:
- id: RG-UNIT-ALL
name: "Все unit-тесты проекта зелёные"
input: "pytest tests/unit/ -v"
expected: "exit-code 0"
- id: RG-INTEG-ALL
name: "Все integration-тесты проекта зелёные"
input: "pytest tests/integration/ -v"
expected: "exit-code 0"
- id: RG-LINT
name: "Линтеры зелёные"
input: "make lint"
expected: "exit-code 0"
- name: ui-playwright
type: ui
description: "Playwright UI-тесты на test-среде"
reference: "04b-ui-test-cases.md"
cases:
- id: UI-LINK-01
name: "См. 04b-ui-test-cases.md — TC-UI-01..TC-UI-12"
expected: |
Каждый TC выполняется; check-visual подтверждается
оператором либо визуальным diff-инструментом
(baseline до ET-013 vs текущий).
- name: manual-deploy-validation
type: e2e
description: "Ручная проверка в test-среде после деплоя"
marker: "manual"
cases:
- id: E2E-PRE-DEPLOY-01
name: "Pre-deploy: тайлы z9-z11 на test-среде доступны"
steps:
- "curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/308/158.png | head -1"
- "curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/10/617/317.png | head -1"
- "curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/11/1234/635.png | head -1"
- "Все три — HTTP/1.1 200 OK. При 404 — стоп, открыть PH-6 follow-up."
- "Зафиксировать в 14-deploy-log.md."
- id: E2E-DEPLOY-01
name: "Hillshade доступен на z=9"
steps:
- "Открыть https://openclaw.mva154.duckdns.org/enduro/"
- "localStorage.clear(); location.reload()"
- "Click #terrain-toggle"
- "В Console: window._map.setZoom(9); window._map.setCenter([37.6, 54.5])"
- "Wait 2s"
- "Кнопка #terrain-hillshade-cb имеет disabled=false"
- "Hint #terrain-hillshade-hint имеет display:none"
- "Click #terrain-hillshade-cb"
- "Wait 3s"
- "На карте видны тени"
- "Screenshot et013-deploy-z9.png"
- "Зафиксировать в 14-deploy-log.md"
- id: E2E-DEPLOY-02
name: "Network-объём: рост ≤ 35%"
steps:
- "Открыть DevTools Network, фильтр /terrain/"
- "Очистить network log"
- "В Console: window._map.setZoom(8); ждать 3s; setZoom(9); ждать 3s; setZoom(10); ждать 3s; setZoom(11); ждать 3s"
- "Замерить суммарный transferred size в фильтре /terrain/"
- "Сравнить с baseline (записан в 13-test-report.md до ET-013): рост ≤ 135%"
- "Зафиксировать"
- id: E2E-DEPLOY-03
name: "Регрессия z=8 (TRI выглядит как до ET-013)"
steps:
- "localStorage.clear(); location.reload()"
- "Включить только #terrain-tri-cb (без hillshade)"
- "window._map.setZoom(8); setCenter([37.6, 54.5])"
- "Screenshot et013-deploy-z8-tri-regress.png"
- "Визуально сравнить с baseline из 13-test-report.md до ET-013 — не отличается заметно."
- id: E2E-DEPLOY-04
name: "Регрессия z=14 (hillshade не перегрет)"
steps:
- "Включить #terrain-hillshade-cb"
- "window._map.setZoom(14); setCenter([37.6, 54.5])"
- "Screenshot et013-deploy-z14-regress.png"
- "Эффективное raster-opacity ≈ 0.40, raster-contrast ≈ 0"
- "В Console: window._map.getPaintProperty('terrain-hillshade', 'raster-opacity')"
- "(вернёт interpolate-выражение — proof zoom-aware)"
- id: E2E-DEPLOY-05
name: "Спутник + hillshade на z=10 (R-3)"
steps:
- "Включить hillshade, переключить #base-btn-satellite"
- "window._map.setZoom(10); setCenter([37.6, 54.5])"
- "Screenshot et013-deploy-z10-sat.png"
- "Визуальная приёмка: hillshade видим, не глушит снимок"
- "При проблеме — задача отправляется на корректировку stops"
- id: E2E-DEPLOY-06
name: "Тёмная тема + hillshade на z=10 (R-2)"
steps:
- "Click #btn-theme (переключить в тёмную)"
- "window._map.setZoom(10)"
- "Screenshot et013-deploy-z10-dark.png"
- "Визуальная приёмка: hillshade читается, не сливается с тёмной подложкой"
- id: E2E-DEPLOY-07
name: "Persistence: F5 не теряет состояние"
steps:
- "Включить оба чекбокса"
- "location.reload()"
- "Чекбоксы остаются включёнными"
- "На текущем zoom оба слоя восстановлены"
test_data:
fixtures_dir: "tests/fixtures/terrain/"
fixtures:
- name: "hillshade-z9-sample.png"
description: |
Опционально: один валидный PNG-тайл из data/terrain/hillshade/9/
для CI-окружения без полного набора данных. Скопировать любой
тайл над ЦФО, переименовать. ~10 KB.
- name: "hillshade-z10-sample.png"
description: "То же для z10."
- name: "tri-z10-sample.png"
description: "TRI sample для z10."
notes:
- "Если на CI нет TERRAIN_DIR с данными — IT-TILE-* тесты skipped (REQ-F-15)."
- "Сравнения 'до/после' визуальные — baseline скриншоты лежат в 13-test-report.md и фиксируются до начала ET-013."
- "Для unit-тестов paint никаких fixture не нужно — парсинг исходника."
test_environment:
unit:
- "Python 3.12, pytest"
- "regex-парсер src/web/app.js (Вариант B в TRZ REQ-F-13)"
- "Опционально Node + JSDOM, если в проекте появятся JS-тесты"
integration:
- "FastAPI TestClient против src.api.main:app"
- "TERRAIN_DIR через env или skip-if-missing"
performance:
- "Не требуется специально: NFR-01/02 говорят о невидимом изменении render-time"
- "Сетевой объём — ручной замер в DevTools Network (E2E-DEPLOY-02)"
e2e:
- "Test-среда https://openclaw.mva154.duckdns.org/enduro/"
- "Playwright (см. 04b-ui-test-cases.md)"
ci_gates:
- "Unit UT-PAINT-* и UT-REG-* — обязательны (AC-15)"
- "Integration IT-TILE-* — обязательны (с skipif для отсутствующих данных) (AC-16)"
- "Регрессия RG-UNIT-ALL, RG-INTEG-ALL, RG-LINT — обязательны (AC-17, AC-18)"
- "Pre-deploy E2E-PRE-DEPLOY-01 — ручной gate перед merge (AC-19)"
- "UI-тесты Playwright — после деплоя, фиксация в 13-test-report.md"
- "E2E-DEPLOY-01..07 — ручные шаги в 14-deploy-log.md"
---

View File

@@ -0,0 +1,386 @@
---
type: ui-test-cases
work_item_id: ET-013
title: "UI Test Cases: Перепады высот на z9-z11"
version: 1
status: draft
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
- "agent:analyst"
related:
- "PH-6.terrain"
- "ET-007"
---
# UI Test Cases — ET-013: Перепады высот на zoom z9-z11
Базовый URL: `https://openclaw.mva154.duckdns.org/enduro/`
ET-013 — frontend-калибровка: hillshade и TRI используют
zoom-aware paint, UI-минзум hillshade понижен с 10 до 9. UI-тесты
проверяют:
1. На z9 чекбокс «Тени рельефа» активен, hint скрыт, hillshade виден.
2. На z9-z11 перепады «бросаются в глаза» (качественно).
3. На z8 регрессии нет (TRI выглядит как было).
4. На z14 hillshade не «перегрет» (регрессия).
5. Тёмная тема и спутник совместимы.
6. Мобильный viewport работает.
7. Persistence (localStorage) переживает F5.
Селекторы (из текущего `index.html`):
- `#terrain-toggle` — кнопка попапа слоёв рельефа (правая панель).
- `#terrain-popup` — сам попап со списком чекбоксов.
- `#terrain-hillshade-cb` — чекбокс «Тени рельефа».
- `#terrain-hillshade-hint` — hint «Зум 9+» (ET-013) / «Зум 10+» (до ET-013).
- `#terrain-tri-cb` — чекбокс «Перепады».
- `#base-btn-satellite` — кнопка спутника.
- `#btn-theme` — переключатель тёмная/светлая.
- `#map` — карта.
Все тесты выставляют zoom программно через `page.evaluate`:
```js
window._map.setZoom(N);
window._map.setCenter([37.6, 54.5]); // юг МО / Ока, холмистый район
```
Координата `[37.6, 54.5]` (юг Москвы / Кашира / Ока) выбрана как
«заведомо холмистая зона ЦФО» с явным TRI/hillshade.
Скриншоты складываются в `docs/work-items/ET-013/screenshots/`
и пришиваются к `13-test-report.md`. Для качественных AC-07/08/09
оператор сравнивает с baseline скриншотами «до ET-013» (тоже в
`screenshots/baseline/`).
---
### TC-UI-01-Z9 — На z=9 hillshade доступен и виден
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. evaluate: window._map.setZoom(9); window._map.setCenter([37.6, 54.5]);
8. wait: 3000
9. click: "#terrain-toggle"
10. wait: 800
11. screenshot: "et013-01-z9-popup"
12. check-visual: "В попапе #terrain-popup чекбокс «Тени рельефа» (#terrain-hillshade-cb) НЕ disabled, текст не серый. Hint #terrain-hillshade-hint имеет display:none (текст «Зум 9+» не виден). Чекбокс «Перепады» (#terrain-tri-cb) также доступен."
13. click: "#terrain-hillshade-cb"
14. click: "#terrain-tri-cb"
15. wait: 4000
16. screenshot: "et013-01-z9-tracks-visible"
17. check-visual: "На карте при zoom=9 виден район юга Москвы / Оки. Поверх подложки нарисованы тени рельефа (hillshade) — тёмные склоны заметны на холмах вдоль реки. TRI («Перепады») рисует цветные пятна шероховатых зон. Оба слоя читаются, рельеф выразительный."
---
### TC-UI-02-Z8-REGRESS — Регрессия z=8: TRI выглядит как до ET-013
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. wait: 800
9. click: "#terrain-tri-cb"
10. wait: 2000
11. evaluate: window._map.setZoom(8); window._map.setCenter([37.6, 54.5]);
12. wait: 4000
13. screenshot: "et013-02-z8-tri-regress"
14. check-visual: "На z=8 виден слой «Перепады» в опубликованном виде PH-6: opacity ~0.70, ресемпл «жёсткий» (граница 30-метровых клеток SRTM может быть видна, но это норма после ET-013). Слой hillshade выключен. Сравнение с baseline скриншотом 'before-ET-013-z8.png' — визуально близко, без явных регрессий."
---
### TC-UI-03-Z9-Q — Качественная читаемость перепадов на z=9
- тип: ui
- viewport: desktop
- условие: оба слоя включены
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. wait: 500
9. click: "#terrain-hillshade-cb"
10. click: "#terrain-tri-cb"
11. wait: 2000
12. evaluate: window._map.setZoom(9); window._map.setCenter([37.6, 54.5]);
13. wait: 5000
14. screenshot: "et013-03-z9-readable"
15. check-visual: "На z=9 рельеф читается явно: тени по склонам холмов, цветные пятна TRI выделяют шероховатые зоны (склоны вдоль Оки, овраги). Не должно быть впечатления 'плоской карты'. Оператор сравнивает с baseline 'before-ET-013-z9.png' и подтверждает: 'перепады стали выразительнее' или 'минимум не хуже z8'. При отказе — фиксировать в 13-test-report.md и итеративно корректировать stops в HILLSHADE_PAINT/TRI_PAINT."
---
### TC-UI-04-Z10-Q — Качественная читаемость на z=10
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. click: "#terrain-hillshade-cb"
9. click: "#terrain-tri-cb"
10. wait: 2000
11. evaluate: window._map.setZoom(10); window._map.setCenter([37.6, 54.5]);
12. wait: 5000
13. screenshot: "et013-04-z10-readable"
14. check-visual: "На z=10 в фокусе несколько холмов с явными склонами. Hillshade рисует тени с выраженным контрастом (raster-contrast 0.35 в paint-выражении). TRI выделяет шероховатости. Сравнение с baseline 'before-ET-013-z10.png' — стало явно выразительнее. Подложка под слоями ещё читается (opacity 0.60 + 0.85 не превращают карту в кашу)."
---
### TC-UI-05-Z11-Q — Качественная читаемость на z=11
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. click: "#terrain-hillshade-cb"
9. click: "#terrain-tri-cb"
10. wait: 2000
11. evaluate: window._map.setZoom(11); window._map.setCenter([37.6, 54.5]);
12. wait: 5000
13. screenshot: "et013-05-z11-readable"
14. check-visual: "На z=11 виден небольшой район (несколько км в кадре). Перепады «прорисованы», отдельные склоны различимы. Сравнение с baseline 'before-ET-013-z11.png' — выразительнее. Дороги/грунтовки/POI остаются читаемыми поверх рельефа (z-order: terrain ниже trails/POI, проверено по applyTerrainLayer)."
---
### TC-UI-06-Z14-REGRESS — Регрессия z=14: hillshade не перегрет
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. click: "#terrain-hillshade-cb"
9. wait: 2000
10. evaluate: window._map.setZoom(14); window._map.setCenter([37.6, 54.5]);
11. wait: 5000
12. screenshot: "et013-06-z14-regress"
13. check-visual: "На z=14 hillshade выглядит так, как до ET-013: лёгкая «плёнка» теней с opacity ≈ 0.40 и raster-contrast ≈ 0. Никакого перегретого контраста. Подложка отчётливо видна. Сравнение с baseline 'before-ET-013-z14.png' — без отличий."
---
### TC-UI-07-Z9-MOBILE — Hillshade на мобильном viewport на z=9
- тип: ui
- viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. evaluate: window._map.setZoom(9); window._map.setCenter([37.6, 54.5]);
8. wait: 3000
9. click: "#terrain-toggle"
10. wait: 800
11. screenshot: "et013-07-z9-mobile-popup"
12. check-visual: "На мобильном viewport (375×667) попап рельефа открыт, чекбокс «Тени рельефа» доступен, hint скрыт. Чекбокс «Перепады» доступен. Layout не сломан."
13. click: "#terrain-hillshade-cb"
14. click: "#terrain-tri-cb"
15. wait: 4000
16. screenshot: "et013-07-z9-mobile-tracks"
17. check-visual: "На мобильном на z=9 видны тени рельефа и пятна TRI. Перепады читаются. Layout верхней/нижней панелей не перекрывает карту."
---
### TC-UI-08-Z10-SAT-Q — Спутник + hillshade на z=10
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. click: "#base-btn-satellite"
9. wait: 4000
10. click: "#terrain-hillshade-cb"
11. wait: 2000
12. evaluate: window._map.setZoom(10); window._map.setCenter([37.6, 54.5]);
13. wait: 5000
14. screenshot: "et013-08-z10-sat"
15. check-visual: "На спутниковой подложке поверх космоснимка видны тени hillshade. Подложка под ними различима — деревья, реки, поля по-прежнему читаются. Hillshade не превращает снимок в «серую плёнку». При отказе (слой глушит снимок) — открыть итерацию: либо снизить opacity на спутнике через отдельный layer-paint, либо документировать как known issue."
---
### TC-UI-09-Z10-DARK-Q — Тёмная тема + hillshade на z=10
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: localStorage.setItem('theme', 'dark'); location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. click: "#terrain-hillshade-cb"
9. click: "#terrain-tri-cb"
10. wait: 2000
11. evaluate: window._map.setZoom(10); window._map.setCenter([37.6, 54.5]);
12. wait: 5000
13. screenshot: "et013-09-z10-dark"
14. check-visual: "На тёмной теме при z=10 видны и hillshade, и TRI. Тени не сливаются с тёмной подложкой. Цвета TRI читаются. Если визуально слои «съедают карту» — фиксируется как известная проблема для будущей итерации (theme-specific paint, ADR-0001 в follow-up)."
---
### TC-UI-10-PERSIST — Состояние слоёв переживает F5
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. click: "#terrain-hillshade-cb"
9. click: "#terrain-tri-cb"
10. wait: 1500
11. evaluate: window._map.setZoom(10); window._map.setCenter([37.6, 54.5]);
12. wait: 4000
13. screenshot: "et013-10a-before-reload"
14. check-visual: "Оба слоя видны на z=10."
15. evaluate: location.reload();
16. wait: 6000
17. evaluate: window._map.setZoom(10); window._map.setCenter([37.6, 54.5]);
18. wait: 4000
19. screenshot: "et013-10b-after-reload"
20. check-visual: "После reload оба слоя автоматически восстановились (через restoreTerrainState). Чекбоксы в #terrain-popup всё ещё checked. localStorage 'terrain-hillshade'='1', 'terrain-tri'='1'."
---
### TC-UI-11-NETWORK-Q — Сетевой объём (M-10)
- тип: ui (network)
- viewport: desktop
- инструмент: DevTools Network
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. open: DevTools Network, filter "/terrain/"
8. clear network log
9. click: "#terrain-toggle"
10. click: "#terrain-hillshade-cb"
11. click: "#terrain-tri-cb"
12. evaluate: window._map.setZoom(8); window._map.setCenter([37.6, 54.5]);
13. wait: 3500
14. evaluate: window._map.setZoom(9);
15. wait: 3500
16. evaluate: window._map.setZoom(10);
17. wait: 3500
18. evaluate: window._map.setZoom(11);
19. wait: 3500
20. record: суммарный transferred size в Network
21. check-visual: "Сравнение с baseline 'before-ET-013-network-z8-z11.txt' (записанным до начала ET-013): рост ≤ 135%. Если выше — анализ: какие тайлы добавились, оправдано ли. Фиксация в 13-test-report.md."
---
### TC-UI-12-Z9-PAN — Панорамирование на z=9 без лагов
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. evaluate: localStorage.clear();
4. wait: 500
5. evaluate: location.reload();
6. wait: 5000
7. click: "#terrain-toggle"
8. click: "#terrain-hillshade-cb"
9. click: "#terrain-tri-cb"
10. wait: 2000
11. evaluate: window._map.setZoom(9); window._map.setCenter([37.6, 54.5]);
12. wait: 5000
13. evaluate: window._map.panBy([400, 0]);
14. wait: 3000
15. evaluate: window._map.panBy([0, 400]);
16. wait: 3000
17. evaluate: window._map.panBy([-400, 0]);
18. wait: 3000
19. screenshot: "et013-12-z9-pan"
20. check-visual: "После трёх pan-шагов карта показывает соседние регионы. Тайлы догружены, нет 'белых дыр' в hillshade/TRI. Возврат к исходному центру — мгновенный (browser cache). UI не блокируется, нет визуальных лагов."
---
### Заметки по запуску
- TC-UI-03..05 (Q-критерии) — качественные. Оператор сравнивает
скриншот с baseline («до ET-013»). Baseline записывается **до**
начала разработки ET-013 и кладётся в
`docs/work-items/ET-013/screenshots/baseline/`.
- TC-UI-08 (SAT-Q) и TC-UI-09 (DARK-Q) — допустимо «known issue»
с фиксацией в `13-test-report.md`. Если визуальная регрессия
обнаружена — открывается follow-up задача по theme/sat-specific paint.
- При отказе TC-UI-03/04/05 — корректировка stops в
`HILLSHADE_PAINT`/`TRI_PAINT`, новый прогон. Это калибровка, а не баг.
- При отказе TC-UI-06 (z14 регрессия) — баг калибровки stops,
должен быть исправлен.
- TC-UI-11 (NETWORK-Q) — pre/post замеры; baseline записывается
до старта работ над ET-013.
### Координаты для тестов
| Координаты | Регион | Зачем |
|---|---|---|
| `[37.6, 54.5]` | юг МО / Кашира / Ока | холмистый, выраженный hillshade и TRI |
| `[37.6, 55.7]` | центр Москвы | плоский, контроль «город всё равно читается» (опционально) |
| `[38.6, 54.0]` | Тула | холмы юга ЦФО, альтернатива для AC-08 |
По умолчанию все TC используют `[37.6, 54.5]`.