233 lines
27 KiB
Markdown
233 lines
27 KiB
Markdown
---
|
||
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.
|