Files
enduro-trails/docs/work-items/ET-013/01-brd.md
claude-bot 7df1ffe75c
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 9s
CI / build (push) Successful in 2s
analyst(ET): auto-commit from analyst run_id=78
2026-06-04 09:28:51 +00:00

27 KiB
Raw Blame History

type, work_item_id, title, version, status, created_at, updated_at, authors, related
type work_item_id title version status created_at updated_at authors related
brd ET-013 BRD: Сохранить выразительность перепадов высот на z9-z11 1 draft 2026-06-04 2026-06-04
agent:analyst
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):

// Строка ~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):

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-hillshadeinterpolate-выражение со 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-triinterpolate-выражение со 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).
    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.