auto-sync: 2026-05-09 20:40:01
This commit is contained in:
330
tasks/enduro-trails/DEV_TASK_TERRAIN.md
Normal file
330
tasks/enduro-trails/DEV_TASK_TERRAIN.md
Normal file
@@ -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: `<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 не запрашивал тайлы вне региона
|
||||
@@ -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 |
|
||||
|
||||
85
tasks/enduro-trails/prototype/scripts/download_srtm.js
vendored
Normal file
85
tasks/enduro-trails/prototype/scripts/download_srtm.js
vendored
Normal file
@@ -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
|
||||
});
|
||||
91
tasks/enduro-trails/prototype/scripts/download_srtm_v2.js
vendored
Normal file
91
tasks/enduro-trails/prototype/scripts/download_srtm_v2.js
vendored
Normal file
@@ -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
|
||||
});
|
||||
94
tasks/enduro-trails/prototype/scripts/download_srtm_v3.js
vendored
Normal file
94
tasks/enduro-trails/prototype/scripts/download_srtm_v3.js
vendored
Normal file
@@ -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
|
||||
});
|
||||
20
tasks/enduro-trails/reports/dev-2026-05-09-terrain.md
Normal file
20
tasks/enduro-trails/reports/dev-2026-05-09-terrain.md
Normal file
@@ -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 + генерации тайлов.
|
||||
Reference in New Issue
Block a user