# 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 не запрашивал тайлы вне региона
