11 KiB
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
Пример скрипта:
# 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:
# Собрать все .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)
# 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):
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):
// 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:
mountainicon
Попап
<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>
Логика
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)
.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. Деплой
- Сгенерировать тайлы на сервере (или локально + SFTP)
- Скопировать тайлы в
/home/slin/enduro-trails/data/terrain/ - Обновить nginx конфиг, перезагрузить
- Деплой фронтенда:
deploy_static.js+docker cpпосле рестарта - Проверить: открыть карту, включить рельеф, проверить тайлы в 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 не запрашивал тайлы вне региона