--- 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.