diff --git a/tasks/enduro-trails/DEV_TASK_TERRAIN.md b/tasks/enduro-trails/DEV_TASK_TERRAIN.md new file mode 100644 index 0000000..e33ae46 --- /dev/null +++ b/tasks/enduro-trails/DEV_TASK_TERRAIN.md @@ -0,0 +1,330 @@ +# 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: `` +- Или Lucide: `mountain` icon + +#### Попап +```html +
+``` + +#### Логика +```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 не запрашивал тайлы вне региона diff --git a/tasks/enduro-trails/PROJECT.md b/tasks/enduro-trails/PROJECT.md index bb8c74f..11a02f8 100644 --- a/tasks/enduro-trails/PROJECT.md +++ b/tasks/enduro-trails/PROJECT.md @@ -97,6 +97,7 @@ docker restart prototype-enduro-trails-1 | F-14 | "Разведка" | Грунтовки вокруг точки, статистика по типам, POI | ⏳ Бэклог | 4 | | F-15 | "Народные треки" | OSM Traces, Wikiloc, Komoot, 4x4travel | ⏳ Бэклог | 8 | | F-16 | Тёмная тема + редизайн | Две темы (авто/светлая/тёмная), SunCalc, мобильный UI, drag-and-drop точек, расстояние по маршруту | ✅ Готово | 5 | +| F-29 | Рельеф (terrain) | Цветной рельеф (все зумы) + теневой (зум 10+). SRTM 30м, кнопка 🏔️ в toolbar с чекбоксами | 📋 BRD готов | 5.4 | | F-22 | Линейка UX | Расстояние сегмента под маркером, крестик удаления, зелёный Старт, панель fit-content, toast, toggle скрыть/показать, deleteRuler | ✅ Готово | 5.2 | | F-23 | Метки UX | Починен баг удаления через попап (popup.remove() перед marker.remove()) | ✅ Готово | 5.2 | | F-24 | Поиск точек маршрута | Inline Nominatim поиск в каждом wl-item, убран верхний search bar | ✅ Готово | 5.2 | @@ -203,10 +204,18 @@ docker restart prototype-enduro-trails-1 - `streaming.mode: "off"` в Telegram канале — убраны дублированные сообщения - `send_voice.sh` — убрана отправка через `openclaw message send`, только генерация OGG + `MEDIA:` директива -### ⏳ Фаза 6 — SRTM рельеф +### 📋 Фаза 5.4 — Рельеф на карте +**BRD:** `BRD_TERRAIN.md` +**DEV TASK:** `DEV_TASK_TERRAIN.md` +- F-29 Рельеф (terrain) — цветной (все зумы) + теневой (зум 10+) +- SRTM 30м, GDAL, растровые тайлы PNG +- Кнопка 🏔️ в toolbar с чекбоксами, localStorage +- nginx статика `/enduro/terrain/` + +### ⏳ Фаза 6 — SRTM рельеф (продвинутый) - F-12 «Горка» — макс набор высоты, мин дистанция - Профиль высот на маршруте -- SRTM DEM 30м данные (Public Domain) +- SRTM данные уже есть после фазы 5.4 ### ⏳ Фаза 7 — PWA + офлайн - F-17 Service Worker, офлайн MBTiles, GPS-трекинг в реальном времени @@ -252,6 +261,8 @@ docker restart prototype-enduro-trails-1 | `BRD_PHASE3.md` | Бизнес-требования Фазы 3 (согласовано) | | `BRD_PHASE3.1.md` | Бизнес-требования Фазы 3.1 (на согласовании) | | `BRD_PHASE5.md` | Бизнес-требования Фазы 5 (✅ реализовано) | +| `BRD_TERRAIN.md` | Бизнес-требования Фазы 5.4 — Рельеф | +| `DEV_TASK_TERRAIN.md` | ТЗ для Dev-агента Фаза 5.4 | | `TEST_CASES_PHASE3.md` | 56 тест-кейсов | | `DEV_TASK_PHASE3.md` | ТЗ для Dev-агента Фаза 3 | | `DEV_TASK_PHASE5.md` | ТЗ для Dev-агента Фаза 5 | diff --git a/tasks/enduro-trails/prototype/scripts/download_srtm.js b/tasks/enduro-trails/prototype/scripts/download_srtm.js new file mode 100644 index 0000000..aecf24d --- /dev/null +++ b/tasks/enduro-trails/prototype/scripts/download_srtm.js @@ -0,0 +1,85 @@ +const { Client } = require('ssh2'); + +const conn = new Client(); + +conn.on('ready', () => { + console.log('SSH connected'); + + const downloadScript = [ + '#!/bin/bash', + 'set -e', + 'SRTM_DIR="/home/slin/enduro-trails/data/srtm"', + 'mkdir -p "$SRTM_DIR"', + 'BASE_URL="https://s3.amazonaws.com/elevation-tiles-prod/skadi"', + 'TILES=(', + ' "N55E037" "N55E038" "N55E039" "N55E040"', + ' "N54E037" "N54E038" "N54E039" "N54E040"', + ' "N53E038" "N53E039" "N53E040" "N53E041"', + ' "N52E038" "N52E039" "N52E040" "N52E041"', + ' "N56E037" "N56E038" "N56E039" "N56E040"', + ' "N57E037" "N57E038" "N57E039" "N57E040"', + ' "N58E037" "N58E038" "N58E039" "N58E040"', + ' "N59E038" "N59E039" "N59E040" "N59E041"', + ' "N60E040" "N60E041" "N60E042"', + ' "N54E042" "N54E043" "N54E044" "N54E045"', + ' "N53E042" "N53E043" "N53E044" "N53E045"', + ' "N52E042" "N52E043" "N52E044" "N52E045"', + ' "N51E038" "N51E039" "N51E040" "N51E041"', + ' "N50E038" "N50E039" "N50E040" "N50E041"', + ' "N55E047" "N55E048" "N55E049" "N55E050"', + ' "N54E047" "N54E048" "N54E049" "N54E050"', + ' "N56E047" "N56E048" "N56E049" "N56E050"', + ')', + 'echo "Downloading ${TILES[@]} SRTM tiles..."', + 'cd "$SRTM_DIR"', + 'for tile in "${TILES[@]}"; do', + ' lat=${tile:1:2}', + ' fname="${tile}.hgt.gz"', + ' url="${BASE_URL}/${lat}/${fname}"', + ' if [ -f "${tile}.hgt" ]; then', + ' echo "SKIP ${tile} (already have .hgt)"', + ' continue', + ' fi', + ' if [ -f "${fname}" ]; then', + ' echo "SKIP ${tile} (already downloaded)"', + ' continue', + ' fi', + ' echo "DOWNLOAD ${tile}..."', + ' wget --timeout=60 -q "$url" -O "${fname}"', + ' if [ -s "${fname}" ]; then', + ' echo "EXTRACT ${tile}..."', + ' gunzip -f "${fname}"', + ' echo "OK ${tile}"', + ' else', + ' echo "FAIL ${tile} (empty or error)"', + ' rm -f "${fname}"', + ' fi', + 'done', + 'echo ""', + 'echo "Download complete. Files:"', + 'ls -la *.hgt 2>/dev/null | wc -l', + 'echo " .hgt files"', + ].join('\n'); + + conn.exec(downloadScript, (err, stream) => { + if (err) { console.error('Exec error:', err); conn.end(); return; } + stream.on('data', d => process.stdout.write(d)); + stream.stderr.on('data', d => process.stderr.write(d)); + stream.on('close', (code) => { + console.log('\nDownload script exited:', code); + conn.end(); + }); + }); +}); + +conn.on('error', (err) => { + console.error('SSH error:', err.message); + process.exit(1); +}); + +conn.connect({ + host: '82.22.50.71', + username: 'slin', + password: 'motoZ@yaz2010', + readyTimeout: 30000 +}); diff --git a/tasks/enduro-trails/prototype/scripts/download_srtm_v2.js b/tasks/enduro-trails/prototype/scripts/download_srtm_v2.js new file mode 100644 index 0000000..f13ec70 --- /dev/null +++ b/tasks/enduro-trails/prototype/scripts/download_srtm_v2.js @@ -0,0 +1,91 @@ +const { Client } = require('ssh2'); + +const TILES = [ + 'N55E037','N55E038','N55E039','N55E040', + 'N54E037','N54E038','N54E039','N54E040', + 'N53E038','N53E039','N53E040','N53E041', + 'N52E038','N52E039','N52E040','N52E041', + 'N56E037','N56E038','N56E039','N56E040', + 'N57E037','N57E038','N57E039','N57E040', + 'N58E037','N58E038','N58E039','N58E040', + 'N59E038','N59E039','N59E040','N59E041', + 'N60E040','N60E041','N60E042', + 'N54E042','N54E043','N54E044','N54E045', + 'N53E042','N53E043','N53E044','N53E045', + 'N52E042','N52E043','N52E044','N52E045', + 'N51E038','N51E039','N51E040','N51E041', + 'N50E038','N50E039','N50E040','N50E041', + 'N55E047','N55E048','N55E049','N55E050', + 'N54E047','N54E048','N54E049','N54E050', + 'N56E047','N56E048','N56E049','N56E050', +]; + +const conn = new Client(); + +conn.on('ready', () => { + console.log('SSH connected. Downloading SRTM tiles...'); + + let completed = 0; + let failed = 0; + + function downloadNext(idx) { + if (idx >= TILES.length) { + console.log(`\nAll done. Completed: ${completed}, Failed: ${failed}`); + conn.end(); + return; + } + + const tile = TILES[idx]; + const lat = tile.substring(1, 3); + const url = `https://s3.amazonaws.com/elevation-tiles-prod/skadi/${lat}/${tile}.hgt.gz`; + const remoteHgt = `/home/slin/enduro-trails/data/srtm/${tile}.hgt`; + const remoteGz = `/home/slin/enduro-trails/data/srtm/${tile}.hgt.gz`; + + // Check if already exists + conn.exec(`test -f ${remoteHgt} && echo EXISTS || echo MISSING`, (err, stream) => { + if (err) { failed++; downloadNext(idx + 1); return; } + let out = ''; + stream.on('data', d => out += d); + stream.on('close', () => { + if (out.trim() === 'EXISTS') { + completed++; + console.log(`[${idx+1}/${TILES.length}] SKIP ${tile} (exists)`); + downloadNext(idx + 1); + return; + } + + // Download + conn.exec(`wget --timeout=60 -q "${url}" -O ${remoteGz} && test -s ${remoteGz} && gunzip -f ${remoteGz} && echo OK || (rm -f ${remoteGz}; echo FAIL)`, (err, stream) => { + if (err) { failed++; downloadNext(idx + 1); return; } + let out2 = ''; + stream.on('data', d => out2 += d); + stream.on('close', () => { + const result = out2.trim(); + if (result === 'OK') { + completed++; + console.log(`[${idx+1}/${TILES.length}] OK ${tile}`); + } else { + failed++; + console.log(`[${idx+1}/${TILES.length}] FAIL ${tile}`); + } + downloadNext(idx + 1); + }); + }); + }); + }); + } + + downloadNext(0); +}); + +conn.on('error', (err) => { + console.error('SSH error:', err.message); + process.exit(1); +}); + +conn.connect({ + host: '82.22.50.71', + username: 'slin', + password: 'motoZ@yaz2010', + readyTimeout: 30000 +}); diff --git a/tasks/enduro-trails/prototype/scripts/download_srtm_v3.js b/tasks/enduro-trails/prototype/scripts/download_srtm_v3.js new file mode 100644 index 0000000..4699b16 --- /dev/null +++ b/tasks/enduro-trails/prototype/scripts/download_srtm_v3.js @@ -0,0 +1,94 @@ +const { Client } = require('ssh2'); + +const TILES = [ + 'N55E037','N55E038','N55E039','N55E040', + 'N54E037','N54E038','N54E039','N54E040', + 'N53E038','N53E039','N53E040','N53E041', + 'N52E038','N52E039','N52E040','N52E041', + 'N56E037','N56E038','N56E039','N56E040', + 'N57E037','N57E038','N57E039','N57E040', + 'N58E037','N58E038','N58E039','N58E040', + 'N59E038','N59E039','N59E040','N59E041', + 'N60E040','N60E041','N60E042', + 'N54E042','N54E043','N54E044','N54E045', + 'N53E042','N53E043','N53E044','N53E045', + 'N52E042','N52E043','N52E044','N52E045', + 'N51E038','N51E039','N51E040','N51E041', + 'N50E038','N50E039','N50E040','N50E041', + 'N55E047','N55E048','N55E049','N55E050', + 'N54E047','N54E048','N54E049','N54E050', + 'N56E047','N56E048','N56E049','N56E050', +]; + +const conn = new Client(); + +conn.on('ready', () => { + console.log('SSH connected. Downloading SRTM tiles sequentially...'); + + let completed = 0; + let failed = 0; + let skipped = 0; + + function downloadNext(idx) { + if (idx >= TILES.length) { + console.log(`\n=== DONE === Completed: ${completed}, Skipped: ${skipped}, Failed: ${failed}`); + conn.end(); + return; + } + + const tile = TILES[idx]; + const lat = tile.substring(1, 3); + const url = `https://s3.amazonaws.com/elevation-tiles-prod/skadi/${lat}/${tile}.hgt.gz`; + const remoteHgt = `/home/slin/enduro-trails/data/srtm/${tile}.hgt`; + const remoteGz = `/home/slin/enduro-trails/data/srtm/${tile}.hgt.gz`; + + // Check if already exists + conn.exec(`test -f ${remoteHgt} && echo EXISTS || echo MISSING`, (err, stream) => { + if (err) { failed++; downloadNext(idx + 1); return; } + let out = ''; + stream.on('data', d => out += d); + stream.on('close', () => { + if (out.trim() === 'EXISTS') { + skipped++; + console.log(`[${idx+1}/${TILES.length}] SKIP ${tile} (exists)`); + downloadNext(idx + 1); + return; + } + + // Download with wget (no -q to see errors) + const cmd = `cd /home/slin/enduro-trails/data/srtm && wget --timeout=60 "${url}" -O ${remoteGz} 2>&1 && test -s ${remoteGz} && gunzip -f ${remoteGz} && echo OK || (rm -f ${remoteGz}; echo FAIL)`; + + conn.exec(cmd, (err, stream) => { + if (err) { failed++; downloadNext(idx + 1); return; } + let out2 = ''; + stream.on('data', d => out2 += d); + stream.on('close', () => { + const result = out2.trim().split('\n').pop(); + if (result === 'OK') { + completed++; + console.log(`[${idx+1}/${TILES.length}] OK ${tile}`); + } else { + failed++; + console.log(`[${idx+1}/${TILES.length}] FAIL ${tile}`); + } + downloadNext(idx + 1); + }); + }); + }); + }); + } + + downloadNext(0); +}); + +conn.on('error', (err) => { + console.error('SSH error:', err.message); + process.exit(1); +}); + +conn.connect({ + host: '82.22.50.71', + username: 'slin', + password: 'motoZ@yaz2010', + readyTimeout: 30000 +}); diff --git a/tasks/enduro-trails/reports/dev-2026-05-09-terrain.md b/tasks/enduro-trails/reports/dev-2026-05-09-terrain.md new file mode 100644 index 0000000..18edb14 --- /dev/null +++ b/tasks/enduro-trails/reports/dev-2026-05-09-terrain.md @@ -0,0 +1,20 @@ +# Dev Report: Рельеф (Terrain Layer) +Дата: 2026-05-09 +Статус: IN PROGRESS + +## Задача +Реализовать фичу "Рельеф" (Terrain Layer) для Enduro Trails: +1. Скачать SRTM данные для ЦФО + Чувашия +2. Сгенерировать растровые тайлы PNG (hypsometric + hillshade) +3. Настроить nginx для статики +4. Frontend: MapLibre raster sources + layers +5. UI: кнопка 🏔️ в toolbar с попапом и чекбоксами +6. Деплой + +## Сделано +- [ ] Прочитаны BRD, DEV_TASK, PROJECT +- [ ] Изучена текущая структура фронтенда (index.html, app.js, app.css) +- [ ] Найдены layer toggle controls (map-controls-r div, layerState в app.js) + +## Следующий шаг +Создать отчёт и начать с SSH на сервер для скачивания SRTM + генерации тайлов.