diff --git a/memory/2026-05-03.md b/memory/2026-05-03.md index cc56d16..7e28b2c 100644 --- a/memory/2026-05-03.md +++ b/memory/2026-05-03.md @@ -78,3 +78,15 @@ **openclaw.json изменения:** - `agents.dev.model.primary` = `vibecode/claude-opus-4.7` + +## Линейка — финальный фикс (21:45 UTC) + +Проблема: плашки смещены влево от кружков на скрине Славы. +Причина: `anchor: 'bottom'` для labelMarker крепит нижний-левый угол, не центр. + +Решение (задеплоено): +- Кружок: отдельный маркер, `anchor: 'center'`, строго 10×10px → линия проходит точно через него +- Плашка: отдельный маркер, `anchor: 'center'`, `offset: [0, -20]` → висит ровно над кружком по центру +- Два маркера на точку: `dotMarker` + `labelMarker`, оба в `rulerMarkers[]` + +Деплой: `docker compose up -d --build` на сервере 82.22.50.71, контейнер `prototype-enduro-trails-1` diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index 3925fe5..eec0c57 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -86,7 +86,6 @@ function toggleLayer(group) { // ─── Map init ───────────────────────────────────────────────────────────────── async function initMap() { - // Определяем base path для работы как на корне, так и под /enduro/ const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || ''; const tileBase = window.location.origin + basePath; const style = await fetch(basePath + '/style.json').then(r => r.json()); @@ -107,13 +106,12 @@ async function initMap() { map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right'); map.addControl(new maplibregl.FullscreenControl(), 'top-left'); - // ─── Loading state ──────────────────────────────────────────────────────── map.on('load', () => { document.getElementById('loading').classList.remove('visible'); checkDataAvailability(); initRouteClicks(map); - initSearch(); initRulerClicks(map); + initSearch(); }); map.on('error', (e) => { @@ -125,7 +123,6 @@ async function initMap() { document.getElementById('loading').classList.remove('visible'); }, 15000); - // ─── Stats bar ──────────────────────────────────────────────────────────── map.on('zoom', () => { document.getElementById('zoom-val').textContent = map.getZoom().toFixed(1); }); @@ -136,7 +133,6 @@ async function initMap() { `${lat.toFixed(4)}, ${lng.toFixed(4)}`; }); - // ─── Popups ─────────────────────────────────────────────────────────────── const popup = new maplibregl.Popup({ closeButton: true, closeOnClick: false, @@ -161,7 +157,6 @@ async function initMap() { return labels[t] || t; } - // Клик по грунтовкам ['trails-track', 'trails-path-bridleway', 'trails-asphalt'].forEach(layerId => { map.on('click', layerId, (e) => { const props = e.features[0].properties; @@ -179,7 +174,6 @@ async function initMap() { map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; }); }); - // Клик по POI map.on('click', 'poi-circles', (e) => { const props = e.features[0].properties; const html = ` @@ -191,7 +185,6 @@ async function initMap() { map.on('mouseenter', 'poi-circles', () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', 'poi-circles', () => { map.getCanvas().style.cursor = ''; }); - // Закрыть popup при клике на пустое место map.on('click', (e) => { const features = map.queryRenderedFeatures(e.point, { layers: ['trails-track', 'trails-path-bridleway', 'trails-asphalt', 'poi-circles'], @@ -202,6 +195,7 @@ async function initMap() { async function checkDataAvailability() { try { + const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || ''; const resp = await fetch(basePath + '/api/health'); const data = await resp.json(); if (!data.db_exists) { @@ -212,12 +206,11 @@ async function checkDataAvailability() { } } -// ─── Роутинг ────────────────────────────────────────────────────────────────────────────── +// ─── Роутинг ────────────────────────────────────────────────────────────────── let routeMode = false; let routeStart = null; let routeEnd = null; let routeMarkers = []; -let routeLayer = null; function toggleRouteMode() { routeMode = !routeMode; @@ -252,7 +245,6 @@ function clearRoute() { async function buildRoute() { const map = window._map; document.getElementById('route-status').textContent = '⏳ Строю маршрут...'; - const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || ''; try { const resp = await fetch( @@ -260,8 +252,6 @@ async function buildRoute() { ); if (!resp.ok) throw new Error('Маршрут не найден'); const data = await resp.json(); - - // Рисуем линию if (map.getSource('route')) { map.getSource('route').setData(data); } else { @@ -270,16 +260,10 @@ async function buildRoute() { id: 'route-line', type: 'line', source: 'route', - paint: { - 'line-color': '#0066ff', - 'line-width': 4, - 'line-opacity': 0.85, - }, + paint: { 'line-color': '#0066ff', 'line-width': 4, 'line-opacity': 0.85 }, layout: { 'line-cap': 'round', 'line-join': 'round' } }); } - - // Показываем статистику const p = data.properties; document.getElementById('route-distance').textContent = `${p.distance_km} км`; document.getElementById('route-duration').textContent = `~${p.duration_min} мин`; @@ -290,12 +274,10 @@ async function buildRoute() { } } -// Клик на карте в режиме роутинга function initRouteClicks(map) { map.on('click', (e) => { if (!routeMode) return; const { lng, lat } = e.lngLat; - if (!routeStart) { routeStart = [lng, lat]; const el = document.createElement('div'); @@ -312,34 +294,23 @@ function initRouteClicks(map) { }); } -// ─── Поиск (Nominatim) ──────────────────────────────────────────────────────────────── +// ─── Поиск (Nominatim) ──────────────────────────────────────────────────────── let searchTimeout = null; function initSearch() { const input = document.getElementById('search-input'); const results = document.getElementById('search-results'); - input.addEventListener('input', () => { clearTimeout(searchTimeout); const q = input.value.trim(); - if (q.length < 2) { - results.style.display = 'none'; - return; - } + if (q.length < 2) { results.style.display = 'none'; return; } searchTimeout = setTimeout(() => doSearch(q), 400); }); - input.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - results.style.display = 'none'; - input.blur(); - } + if (e.key === 'Escape') { results.style.display = 'none'; input.blur(); } }); - document.addEventListener('click', (e) => { - if (!e.target.closest('#search-box')) { - results.style.display = 'none'; - } + if (!e.target.closest('#search-box')) results.style.display = 'none'; }); } @@ -347,21 +318,18 @@ async function doSearch(query) { const results = document.getElementById('search-results'); results.innerHTML = '
Поиск...
'; results.style.display = 'block'; - try { const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=6&countrycodes=ru&accept-language=ru`; const resp = await fetch(url, { headers: { 'Accept-Language': 'ru' } }); const data = await resp.json(); - if (!data.length) { results.innerHTML = '
Ничего не найдено
'; return; } - - results.innerHTML = data.map((item, i) => { + results.innerHTML = data.map((item) => { const name = item.display_name.split(',')[0]; const detail = item.display_name.split(',').slice(1, 3).join(',').trim(); - return `
+ return `
${name}
${detail}
`; @@ -372,13 +340,12 @@ async function doSearch(query) { } function selectSearchResult(lat, lon, name) { - const map = window._map; - map.flyTo({ center: [lon, lat], zoom: 13, duration: 800 }); + window._map.flyTo({ center: [lon, lat], zoom: 13, duration: 800 }); document.getElementById('search-results').style.display = 'none'; document.getElementById('search-input').value = name; } -// ─── Линейка ──────────────────────────────────────────────────────────────────────────────── +// ─── Линейка ────────────────────────────────────────────────────────────────── let rulerMode = false; let rulerPoints = []; let rulerMarkers = []; @@ -389,12 +356,10 @@ function toggleRuler() { const btn = document.getElementById('btn-ruler'); if (rulerMode) { btn.classList.add('active'); - btn.title = 'Линейка активна (кликай точки, двойной клик — завершить)'; window._map.getCanvas().style.cursor = 'crosshair'; clearRuler(); } else { btn.classList.remove('active'); - btn.title = 'Измерить расстояние'; window._map.getCanvas().style.cursor = ''; clearRuler(); } @@ -426,51 +391,41 @@ function updateRulerLine() { } else { map.addSource('ruler', { type: 'geojson', data: geojson }); map.addLayer({ - id: 'ruler-line', - type: 'line', - source: 'ruler', - paint: { - 'line-color': '#0088ff', - 'line-width': 2, - 'line-dasharray': [4, 2], - 'line-opacity': 0.9, - } + id: 'ruler-line', type: 'line', source: 'ruler', + paint: { 'line-color': '#0088ff', 'line-width': 2, 'line-dasharray': [4, 2], 'line-opacity': 0.9 } }); } } -function addRulerPoint(lngLat, isLast) { +function addRulerPoint(lngLat) { const map = window._map; const pt = [lngLat.lng, lngLat.lat]; const idx = rulerPoints.length; rulerPoints.push(pt); - // Считаем расстояние от предыдущей точки if (rulerPoints.length > 1) { - const segDist = haversineKm(rulerPoints[rulerPoints.length - 2], pt); - rulerTotal += segDist; + rulerTotal += haversineKm(rulerPoints[rulerPoints.length - 2], pt); } - // Подпись с накопленным расстоянием const label = rulerPoints.length === 1 ? '0 м' : rulerTotal >= 1 ? rulerTotal.toFixed(1) + ' км' : Math.round(rulerTotal * 1000) + ' м'; - // Кружок точно на координатах - const el = document.createElement('div'); - el.style.cssText = 'position:relative;width:10px;height:10px;'; - el.innerHTML = ` -
-
- ${label} - -
- `; - - const marker = new maplibregl.Marker({ element: el, anchor: 'center' }) + // Кружок — anchor: center, строго 10×10 + const dot = document.createElement('div'); + dot.style.cssText = 'width:10px;height:10px;background:#0088ff;border:2px solid #fff;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,0.3);'; + const dotMarker = new maplibregl.Marker({ element: dot, anchor: 'center' }) .setLngLat([lngLat.lng, lngLat.lat]) .addTo(map); - rulerMarkers.push(marker); + // Плашка — отдельный маркер, anchor: center, offset вверх на 20px + const labelEl = document.createElement('div'); + labelEl.style.cssText = 'display:inline-flex;align-items:center;gap:3px;background:rgba(0,0,0,0.75);color:#fff;font-size:11px;font-weight:600;padding:2px 6px;border-radius:3px;white-space:nowrap;'; + labelEl.innerHTML = `${label}`; + const labelMarker = new maplibregl.Marker({ element: labelEl, anchor: 'center', offset: [0, -20] }) + .setLngLat([lngLat.lng, lngLat.lat]) + .addTo(map); + + rulerMarkers.push(dotMarker, labelMarker); updateRulerLine(); } @@ -480,7 +435,6 @@ function removeRulerPoint(idx) { rulerMarkers.forEach(m => m.remove()); rulerMarkers = []; rulerTotal = 0; - // Пересоздаём все маркеры const pts = [...rulerPoints]; rulerPoints = []; pts.forEach(pt => addRulerPoint({ lng: pt[0], lat: pt[1] })); @@ -489,13 +443,11 @@ function removeRulerPoint(idx) { function initRulerClicks(map) { map.on('click', (e) => { if (!rulerMode) return; - addRulerPoint(e.lngLat, false); + addRulerPoint(e.lngLat); }); - map.on('dblclick', (e) => { if (!rulerMode) return; e.preventDefault(); - // Двойной клик = завершить измерение toggleRuler(); }); }