11 KiB
11 KiB
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 и hillshadeupdateHillshadeAvailability()— 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:
# Через ssh2 — скопировать /home/slin/enduro-trails/prototype/static/app.js
# в /home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/app.js
- 1.2 Добавить в конец app.js следующий код:
// ═══════════════════════════════════════════
// 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:
# 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 Проверить что функции на месте:
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-агент