Files
wiki/tasks/enduro-trails/DEV_TASK_TERRAIN.md
2026-05-09 20:40:01 +03:00

331 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DEV TASK: Рельеф (Terrain Layer)
**Статус:** Ready for dev
**BRD:** `BRD_TERRAIN.md`
**Проект:** Enduro Trails
**Фаза:** 5.4 (после 5.3, перед 6)
---
## Задачи
### 1. Скачать SRTM данные
**Регион:** ЦФО + Чувашия (примерно 45°N-60°N, 35°E-50°E)
**Способы:**
- NASA Earthdata (нужен аккаунт): https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL1.003/
- AWS Open Data (публичный, без регистрации): `s3://elevation-tiles-prod/skadi/{N|S}{lat}/{N|S}{lat}{E|W}{lon}.hgt.gz`
- Или EarthExplorer (USGS)
**Формат:** `.hgt.gz` → распаковать в `.hgt`
**Пример скрипта:**
```python
# download_srtm.py
import requests
import os
TILES = [
# ЦФО + Чувашия — список 1°×1° тайлов
(55, 37), (55, 38), (55, 39), (55, 40), # Москва и север
(54, 37), (54, 38), (54, 39), (54, 40), # Тула, Рязань
(53, 38), (53, 39), (53, 40), (53, 41), # Курск, Воронеж
(52, 38), (52, 39), (52, 40), (52, 41), # Белгород
(56, 37), (56, 38), (56, 39), (56, 40), # Ярославль, Кострома
(57, 37), (57, 38), (57, 39), (57, 40), # Вологда
(58, 37), (58, 38), (58, 39), (58, 40), # Архангельск, Коми
(59, 38), (59, 39), (59, 40), (59, 41), # Сыктывкар
(60, 40), (60, 41), (60, 42), # Ухта
(54, 42), (54, 43), (54, 44), (54, 45), # Самара, Ульяновск
(53, 42), (53, 43), (53, 44), (53, 45), # Пенза, Саратов
(52, 42), (52, 43), (52, 44), (52, 45), # Волгоград
(51, 38), (51, 39), (51, 40), (51, 41), # Ростов
(50, 38), (50, 39), (50, 40), (50, 41), # Краснодар (юг)
(55, 47), (55, 48), (55, 49), (55, 50), # Чувашия, Марий Эл
(54, 47), (54, 48), (54, 49), (54, 50), # Татарстан
(56, 47), (56, 48), (56, 49), (56, 50), # Киров
]
BASE_URL = "https://s3.amazonaws.com/elevation-tiles-prod/skadi"
for lat, lon in TILES:
ns = 'N' if lat >= 0 else 'S'
ew = 'E' if lon >= 0 else 'W'
fname = f"{ns}{abs(lat):02d}{ew}{abs(lon):03d}.hgt.gz"
url = f"{BASE_URL}/{ns}{abs(lat):02d}/{fname}"
# download + gunzip
```
**Куда:** `/home/slin/enduro-trails/data/srtm/`
---
### 2. Сгенерировать тайлы
#### 2.1 Цветной рельеф (hypsometric)
**Ramp файл** (`hypso_ramp.txt`):
```
0 45 80 22
50 90 138 58
150 168 198 108
300 212 184 90
500 196 148 32
800 139 90 43
1200 107 68 35
2000 255 255 255
nv 0 0 0 0
```
**Команды GDAL:**
```bash
# Собрать все .hgt в один VRT
gdalbuildvrt srtm.vrt *.hgt
# Цветной рельеф
gdaldem color-relief srtm.vrt hypso_ramp.txt hypso.tif
# Тайлы (gdal2tiles.py или rio-tiler)
# Вариант A: gdal2tiles.py
gdal2tiles.py -z 5-15 --processes=4 hypso.tif terrain_hypso/
# Вариант B: rio-tiler (если GDAL2Tiles медленный)
# Использовать rio-cogeo + rio-tiler для генерации тайлов
```
**Куда:** `/home/slin/enduro-trails/data/terrain/hypso/{z}/{x}/{y}.png`
#### 2.2 Теневой рельеф (hillshade)
```bash
# Hillshade
gdaldem hillshade -az 315 -alt 45 -z 1 srtm.vrt hillshade.tif
# Тайлы
gdal2tiles.py -z 10-15 --processes=4 hillshade.tif terrain_hillshade/
```
**Куда:** `/home/slin/enduro-trails/data/terrain/hillshade/{z}/{x}/{y}.png`
**Важно:** Проверить, что тайлы в формате PNG (не JPEG, нужна прозрачность/opacity контроль в MapLibre).
---
### 3. Настроить nginx
Добавить в конфиг nginx (на сервере `82.22.50.71`):
```nginx
location /enduro/terrain/ {
alias /home/slin/enduro-trails/data/terrain/;
expires 1y;
add_header Cache-Control "public, immutable";
}
```
Перезагрузить nginx.
---
### 4. Frontend: MapLibre layers
В `style.json` (или программно в `app.js`):
```javascript
// Sources
map.addSource('terrain-hypso', {
type: 'raster',
tiles: ['https://openclaw.mva154.duckdns.org/enduro/terrain/hypso/{z}/{x}/{y}.png'],
tileSize: 256,
minzoom: 5,
maxzoom: 15,
bounds: [35, 45, 50, 60] // [west, south, east, north]
});
map.addSource('terrain-hillshade', {
type: 'raster',
tiles: ['https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/{z}/{x}/{y}.png'],
tileSize: 256,
minzoom: 10,
maxzoom: 15,
bounds: [35, 45, 50, 60]
});
// Layers (paint.opacity контролируется через toggle)
map.addLayer({
id: 'terrain-hypso-layer',
type: 'raster',
source: 'terrain-hypso',
layout: { visibility: 'none' }, // по умолчанию выключен
paint: { 'raster-opacity': 0.45 }
});
map.addLayer({
id: 'terrain-hillshade-layer',
type: 'raster',
source: 'terrain-hillshade',
layout: { visibility: 'none' },
paint: { 'raster-opacity': 0.35 }
});
```
**Z-Index:** `terrain-hypso-layer` должен быть ниже дорог, выше background. `terrain-hillshade-layer` — поверх hypso, под дорогами.
---
### 5. Frontend: UI (toolbar + popup)
#### Кнопка в toolbar
- Позиция: после кнопки слоёв (`#layer-toggle`), перед линейкой
- HTML: `<button id="terrain-toggle" class="map-button" title="Рельеф">🏔️</button>`
- Или Lucide: `mountain` icon
#### Попап
```html
<div id="terrain-popup" class="terrain-popup hidden">
<div class="terrain-popup-header">🏔️ Рельеф</div>
<label class="terrain-checkbox">
<input type="checkbox" id="terrain-hypso-check">
<span>Цветной рельеф</span>
</label>
<label class="terrain-checkbox">
<input type="checkbox" id="terrain-hillshade-check">
<span>Теневой рельеф</span>
<small id="hillshade-hint" class="hint">Доступно при приближении</small>
</label>
</div>
```
#### Логика
```javascript
const terrainState = {
hypso: localStorage.getItem('terrain_hypso') === 'true',
hillshade: localStorage.getItem('terrain_hillshade') === 'true'
};
function updateTerrainLayers() {
const zoom = map.getZoom();
// Hypso — всегда если включено
map.setLayoutProperty('terrain-hypso-layer', 'visibility',
terrainState.hypso ? 'visible' : 'none');
// Hillshade — только если включено И зум >= 10
const hillshadeVisible = terrainState.hillshade && zoom >= 10;
map.setLayoutProperty('terrain-hillshade-layer', 'visibility',
hillshadeVisible ? 'visible' : 'none');
// UI: подсветка кнопки если хоть что-то включено
const btn = document.getElementById('terrain-toggle');
btn.classList.toggle('active', terrainState.hypso || terrainState.hillshade);
// UI: hint для hillshade
const hillshadeCheck = document.getElementById('terrain-hillshade-check');
const hint = document.getElementById('hillshade-hint');
if (zoom < 10) {
hillshadeCheck.disabled = true;
hint.style.display = 'block';
} else {
hillshadeCheck.disabled = false;
hint.style.display = 'none';
}
}
// События
map.on('zoom', updateTerrainLayers);
document.getElementById('terrain-hypso-check').addEventListener('change', (e) => {
terrainState.hypso = e.target.checked;
localStorage.setItem('terrain_hypso', terrainState.hypso);
updateTerrainLayers();
});
document.getElementById('terrain-hillshade-check').addEventListener('change', (e) => {
terrainState.hillshade = e.target.checked;
localStorage.setItem('terrain_hillshade', terrainState.hillshade);
updateTerrainLayers();
});
// Toggle popup
document.getElementById('terrain-toggle').addEventListener('click', (e) => {
e.stopPropagation();
document.getElementById('terrain-popup').classList.toggle('hidden');
});
// Закрыть по тапу вне
map.on('click', () => {
document.getElementById('terrain-popup').classList.add('hidden');
});
```
#### CSS (добавить в app.css)
```css
.terrain-popup {
position: absolute;
bottom: 70px;
right: 12px;
background: var(--bg-primary);
border-radius: 12px;
padding: 12px 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
z-index: 1000;
min-width: 180px;
}
.terrain-popup.hidden { display: none; }
.terrain-popup-header {
font-weight: 600;
margin-bottom: 10px;
font-size: 15px;
}
.terrain-checkbox {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
cursor: pointer;
}
.terrain-checkbox input { margin: 0; }
.terrain-checkbox .hint {
display: none;
color: var(--text-muted);
font-size: 11px;
margin-left: auto;
}
#terrain-toggle.active {
background: var(--accent);
color: white;
}
```
---
### 6. Деплой
1. Сгенерировать тайлы на сервере (или локально + SFTP)
2. Скопировать тайлы в `/home/slin/enduro-trails/data/terrain/`
3. Обновить nginx конфиг, перезагрузить
4. Деплой фронтенда: `deploy_static.js` + `docker cp` после рестарта
5. Проверить: открыть карту, включить рельеф, проверить тайлы в Network tab
---
## Проверка (тест-кейсы)
| # | Действие | Ожидаемый результат |
|---|----------|---------------------|
| 1 | Открыть карту, зум 6, нажать 🏔️, включить «Цветной» | Виден цветной рельеф (зелёный→жёлтый→коричневый) |
| 2 | Зум 12, включить «Теневой» | Появляются тени, овраги объёмные |
| 3 | Зум 8, «Теневой» disabled | Чекбокс серый, hint «Доступно при приближении» |
| 4 | Перезагрузить страницу | Состояние чекбоксов сохранено |
| 5 | Выключить оба слоя | Кнопка 🏔️ не подсвечена |
| 6 | Мобильный: тап вне попапа | Попап закрывается |
---
## Примечания
- **GDAL:** Если нет в контейнере — установить `apt-get install gdal-bin` (или собрать отдельно, контейнер на Debian)
- **Время генерации:** ~30-60 мин на все тайлы (зависит от CPU)
- **Место:** ~150 MB тайлов + ~100 MB .hgt = ~250 MB суммарно
- **Рамп цветов:** Можно скорректировать после теста — в BRD предварительная версия
- **Bounds:** Указать bounds в source, чтобы MapLibre не запрашивал тайлы вне региона