Files
wiki/tasks/enduro-trails/DEV_TASK_TERRAIN_JS.md
2026-05-13 00:50:01 +03:00

297 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 JS-логика (баг — функции отсутствуют в app.js)
**Статус:** Ready for dev
**Проект:** enduro-trails
**Фаза:** 5.4 (фикс)
---
## Цель
> Добавить JS-функции `toggleTerrainPopup()` и `onTerrainCheckbox()` в app.js. Кнопка рельеф должна открывать попап, чекбоксы — включать/выключать слои гипсометрии и отмывки на карте.
## Архитектура
HTML-разметка (кнопка `#terrain-toggle`, попап `#terrain-popup`, чекбоксы) и CSS уже на месте. Нужно только JS:
- `toggleTerrainPopup()` — показать/скрыть попап, toggle класс `.active` на кнопке
- `onTerrainCheckbox()` — добавить/удалить raster source+layer для hypso и hillshade
- `updateHillshadeAvailability()` — disable чекбокс hillshade на зуме < 10
- Персистентность через localStorage
## Стек
- MapLibre GL JS (уже подключён, доступен как `window._map`)
- Тайлы terrain: `https://openclaw.mva154.duckdns.org/enduro/terrain/hypso/{z}/{x}/{y}.png` и `.../hillshade/{z}/{x}/{y}.png`
- Формат тайлов: TMS (нужен `scheme: 'tms'` в source)
- Bounds: `[35, 45, 55, 62]`
---
## Инфраструктура
| Параметр | Значение |
|----------|----------|
| Сервер | `slin@82.22.50.71` (пароль: `motoZ@yaz2010`) |
| Контейнер | `prototype-enduro-trails-1` |
| Файл | `/home/slin/enduro-trails/prototype/static/app.js` (2625 строк) |
| URL | `https://openclaw.mva154.duckdns.org/enduro/` |
| Деплой | SSH → docker cp (БЕЗ рестарта!) |
---
## Файловая карта
| Действие | Файл | Ответственность |
|----------|------|-----------------|
| Изменить | `static/app.js` (добавить в конец) | Terrain JS-логика |
---
## Задачи
### Task 1: Добавить terrain JS-функции в app.js
**Файлы:**
- Изменить: `/home/slin/enduro-trails/prototype/static/app.js` — добавить в конец файла
**Шаги:**
- [ ] **1.1** Скачать актуальный app.js с сервера в workspace:
```bash
# Через ssh2 — скопировать /home/slin/enduro-trails/prototype/static/app.js
# в /home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/app.js
```
- [ ] **1.2** Добавить в конец app.js следующий код:
```javascript
// ═══════════════════════════════════════════
// TERRAIN LAYERS (Phase 5.4)
// ═══════════════════════════════════════════
const TERRAIN_BASE_URL = window.location.pathname.replace(/\/[^/]*$/, '') + '/terrain';
function toggleTerrainPopup() {
const popup = document.getElementById('terrain-popup');
const btn = document.getElementById('terrain-toggle');
if (!popup || !btn) return;
const isVisible = popup.style.display !== 'none';
popup.style.display = isVisible ? 'none' : 'block';
btn.classList.toggle('active', !isVisible);
// Close on outside click
if (!isVisible) {
setTimeout(() => {
document.addEventListener('click', closeTerrainOnOutside);
}, 10);
} else {
document.removeEventListener('click', closeTerrainOnOutside);
}
}
function closeTerrainOnOutside(e) {
const popup = document.getElementById('terrain-popup');
const btn = document.getElementById('terrain-toggle');
if (!popup.contains(e.target) && e.target !== btn && !btn.contains(e.target)) {
popup.style.display = 'none';
btn.classList.remove('active');
document.removeEventListener('click', closeTerrainOnOutside);
}
}
function onTerrainCheckbox() {
const map = window._map;
if (!map) return;
const hypsoChecked = document.getElementById('terrain-hypso-cb').checked;
const hillshadeChecked = document.getElementById('terrain-hillshade-cb').checked;
// Save state
localStorage.setItem('terrain-hypso', hypsoChecked ? '1' : '0');
localStorage.setItem('terrain-hillshade', hillshadeChecked ? '1' : '0');
// Update button active state
const btn = document.getElementById('terrain-toggle');
btn.classList.toggle('active', hypsoChecked || hillshadeChecked);
// Apply layers
applyTerrainLayer('terrain-hypso', TERRAIN_BASE_URL + '/hypso/{z}/{x}/{y}.png', hypsoChecked, 0.55, 5, 15);
applyTerrainLayer('terrain-hillshade', TERRAIN_BASE_URL + '/hillshade/{z}/{x}/{y}.png', hillshadeChecked, 0.40, 10, 15);
}
function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
const map = window._map;
if (!map) return;
const sourceId = id + '-source';
if (enabled) {
// Add source if not exists
if (!map.getSource(sourceId)) {
map.addSource(sourceId, {
type: 'raster',
tiles: [tileUrl],
tileSize: 256,
scheme: 'tms',
bounds: [35, 45, 55, 62],
minzoom: minzoom,
maxzoom: maxzoom
});
}
// Add layer if not exists
if (!map.getLayer(id)) {
// Insert before first road/trail layer for correct z-order
const firstTrailLayer = map.getStyle().layers.find(l =>
l.id.startsWith('trails-') || l.id.startsWith('poi-')
);
map.addLayer({
id: id,
type: 'raster',
source: sourceId,
paint: {
'raster-opacity': opacity
},
minzoom: minzoom,
maxzoom: maxzoom
}, firstTrailLayer ? firstTrailLayer.id : undefined);
}
} else {
// Remove layer and source
if (map.getLayer(id)) map.removeLayer(id);
if (map.getSource(sourceId)) map.removeSource(sourceId);
}
}
function updateHillshadeAvailability() {
const map = window._map;
if (!map) return;
const zoom = map.getZoom();
const cb = document.getElementById('terrain-hillshade-cb');
const hint = document.getElementById('terrain-hillshade-hint');
const label = cb ? cb.closest('.terrain-checkbox') : null;
if (zoom < 10) {
if (cb) cb.disabled = true;
if (label) label.classList.add('disabled');
if (hint) hint.style.display = 'inline';
} else {
if (cb) cb.disabled = false;
if (label) label.classList.remove('disabled');
if (hint) hint.style.display = 'none';
}
}
function restoreTerrainState() {
const hypso = localStorage.getItem('terrain-hypso') === '1';
const hillshade = localStorage.getItem('terrain-hillshade') === '1';
const hypsoCb = document.getElementById('terrain-hypso-cb');
const hillshadeCb = document.getElementById('terrain-hillshade-cb');
if (hypsoCb) hypsoCb.checked = hypso;
if (hillshadeCb) hillshadeCb.checked = hillshade;
if (hypso || hillshade) {
onTerrainCheckbox();
}
// Update button active state
const btn = document.getElementById('terrain-toggle');
if (btn) btn.classList.toggle('active', hypso || hillshade);
}
// Hook into map load and zoom changes
(function initTerrain() {
const map = window._map;
if (map) {
map.on('zoomend', updateHillshadeAvailability);
map.on('style.load', () => {
// Re-apply terrain after style change (theme switch)
setTimeout(restoreTerrainState, 100);
});
// Initial state
updateHillshadeAvailability();
restoreTerrainState();
} else {
// Map not ready yet, wait
const interval = setInterval(() => {
if (window._map) {
clearInterval(interval);
window._map.on('zoomend', updateHillshadeAvailability);
window._map.on('style.load', () => {
setTimeout(restoreTerrainState, 100);
});
updateHillshadeAvailability();
restoreTerrainState();
}
}, 500);
}
})();
```
- [ ] **1.3** Загрузить на сервер и docker cp:
```bash
# SFTP upload app.js → /home/slin/enduro-trails/prototype/static/app.js
# Затем:
docker cp /home/slin/enduro-trails/prototype/static/app.js prototype-enduro-trails-1:/app/static/app.js
```
⚠️ НЕ рестартовать контейнер!
- [ ] **1.4** Проверить что функции на месте:
```bash
docker exec prototype-enduro-trails-1 grep -c "toggleTerrainPopup\|onTerrainCheckbox\|applyTerrainLayer" /app/static/app.js
# Ожидаемый результат: >= 3
```
**Критерий готовности:** Функции `toggleTerrainPopup`, `onTerrainCheckbox`, `applyTerrainLayer`, `updateHillshadeAvailability`, `restoreTerrainState` присутствуют в app.js на сервере.
---
## Проверка (Acceptance)
| # | Проверка | Команда / Действие | Ожидаемый результат |
|---|----------|-------------------|---------------------|
| 1 | Функции в app.js | `grep -c toggleTerrainPopup` | >= 1 |
| 2 | Кнопка открывает попап | Клик по 🏔️ | Попап виден |
| 3 | Чекбокс hypso | Включить «Гипсометрия» | Цветной слой на карте |
| 4 | Чекбокс hillshade | Включить «Отмывка» (зум >= 10) | Тени рельефа |
| 5 | Hillshade disabled | Зум < 10 | Чекбокс неактивен, hint «Зум 10+» |
| 6 | Попап закрывается | Клик вне попапа | Попап скрыт |
| 7 | Персистентность | Перезагрузить страницу | Состояние чекбоксов сохранено |
| 8 | Смена темы | Переключить тему | Terrain слои остаются |
---
## Ограничения и контекст
- ⚠️ docker cp БЕЗ рестарта — рестарт перезапишет статику
- ⚠️ SSH через Node.js ssh2 модуль
- ⚠️ Тайлы в формате TMS — обязателен `scheme: 'tms'` в source
- ⚠️ `bounds: [35, 45, 55, 62]` — без этого MapLibre запрашивает тайлы за пределами региона
- ⚠️ Terrain слои должны быть ПОД дорогами/POI (beforeId = первый trails/poi layer)
- ⚠️ После `map.setStyle()` (смена темы) все кастомные source/layer слетают — `restoreTerrainState()` вызывается на `style.load`
- ⚠️ Hillshade тайлы существуют только для зумов 10-15
- 🚫 НЕ трогать index.html и app.css — они уже корректны
---
## Деплой-чеклист
- [ ] app.js скачан с сервера (актуальная версия)
- [ ] Terrain-код добавлен в конец
- [ ] Загружен обратно на сервер
- [ ] docker cp выполнен
- [ ] `grep toggleTerrainPopup` — найдено
- [ ] Кнопка рельеф работает (попап открывается)
- [ ] Гипсометрия включается (цветной слой виден)
---
*Создано: 2026-05-12 | Автор ТЗ: Стрим | Исполнитель: Dev-агент*