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