diff --git a/tasks/enduro-trails/prototype/static/index.html b/tasks/enduro-trails/prototype/static/index.html index e9937a0..a9d0c49 100644 --- a/tasks/enduro-trails/prototype/static/index.html +++ b/tasks/enduro-trails/prototype/static/index.html @@ -339,39 +339,116 @@ function toggleLayer(group) { } // ─── Map init ───────────────────────────────────────────────────────────────── -const map = new maplibregl.Map({ - container: 'map', - style: '/style.json', - center: [40.5, 55.5], - zoom: 7, - minZoom: 4, - maxZoom: 18, - hash: true, -}); -window._map = map; +async function initMap() { + const tileBase = window.location.origin; + const style = await fetch('/style.json').then(r => r.json()); + style.sources['trails-tiles'].tiles = [`${tileBase}/api/tiles/{z}/{x}/{y}.mvt`]; -map.addControl(new maplibregl.NavigationControl(), 'top-left'); -map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right'); -map.addControl(new maplibregl.FullscreenControl(), 'top-left'); + const map = new maplibregl.Map({ + container: 'map', + style: style, + center: [40.5, 55.5], + zoom: 7, + minZoom: 4, + maxZoom: 18, + hash: true, + }); + window._map = map; -// ─── Loading state ──────────────────────────────────────────────────────────── -map.on('load', () => { - document.getElementById('loading').classList.remove('visible'); - checkDataAvailability(); -}); + map.addControl(new maplibregl.NavigationControl(), 'top-left'); + map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right'); + map.addControl(new maplibregl.FullscreenControl(), 'top-left'); -map.on('error', (e) => { - console.error('Map error:', e.error?.message || e); - // Снимаем loading даже при ошибке чтобы не висело - document.getElementById('loading').classList.remove('visible'); -}); + // ─── Loading state ───────────────────────────────────────────────────────────────── + map.on('load', () => { + document.getElementById('loading').classList.remove('visible'); + checkDataAvailability(); + }); -// Таймаут — если load не сработал за 15 сек, снимаем loading -setTimeout(() => { - document.getElementById('loading').classList.remove('visible'); -}, 15000); + map.on('error', (e) => { + console.error('Map error:', e.error?.message || e); + document.getElementById('loading').classList.remove('visible'); + }); + + setTimeout(() => { + document.getElementById('loading').classList.remove('visible'); + }, 15000); + + // ─── Stats bar ───────────────────────────────────────────────────────────────── + map.on('zoom', () => { + document.getElementById('zoom-val').textContent = map.getZoom().toFixed(1); + }); + + map.on('mousemove', (e) => { + const { lng, lat } = e.lngLat; + document.getElementById('coords-val').textContent = + `${lat.toFixed(4)}, ${lng.toFixed(4)}`; + }); + + // ─── Popups ───────────────────────────────────────────────────────────────── + const popup = new maplibregl.Popup({ + closeButton: true, + closeOnClick: false, + maxWidth: '300px', + }); + + function formatLength(m) { + if (!m) return '—'; + if (m >= 1000) return (m / 1000).toFixed(1) + ' км'; + return Math.round(m) + ' м'; + } + + function poiTypeLabel(t) { + const labels = { + 'natural=peak': '⛰ Вершина', + 'natural=water': '💧 Вода', + 'tourism=viewpoint': '👁 Смотровая', + 'historic=ruins': '🏙 Руины', + 'natural=cave_entrance': '🕳 Пещера', + 'ford=yes': '🌊 Брод', + }; + return labels[t] || t; + } + + // Клик по грунтовкам + ['trails-track', 'trails-path-bridleway', 'trails-asphalt'].forEach(layerId => { + map.on('click', layerId, (e) => { + const props = e.features[0].properties; + const html = ` + + + + + + ${props.mtb_scale ? `` : ''} + `; + popup.setLngLat(e.lngLat).setHTML(html).addTo(map); + }); + map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; }); + map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; }); + }); + + // Клик по POI + map.on('click', 'poi-circles', (e) => { + const props = e.features[0].properties; + const html = ` + + + `; + popup.setLngLat(e.lngLat).setHTML(html).addTo(map); + }); + 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'], + }); + if (!features.length) popup.remove(); + }); +} -// ─── Check if data is available ─────────────────────────────────────────────── async function checkDataAvailability() { try { const resp = await fetch('/api/health'); @@ -384,83 +461,8 @@ async function checkDataAvailability() { } } -// ─── Stats bar ──────────────────────────────────────────────────────────────── -map.on('zoom', () => { - document.getElementById('zoom-val').textContent = map.getZoom().toFixed(1); -}); - -map.on('mousemove', (e) => { - const { lng, lat } = e.lngLat; - document.getElementById('coords-val').textContent = - `${lat.toFixed(4)}, ${lng.toFixed(4)}`; -}); - -// ─── Popups ─────────────────────────────────────────────────────────────────── -const popup = new maplibregl.Popup({ - closeButton: true, - closeOnClick: false, - maxWidth: '300px', -}); - -function formatLength(m) { - if (!m) return '—'; - if (m >= 1000) return (m / 1000).toFixed(1) + ' км'; - return Math.round(m) + ' м'; -} - -function poiTypeLabel(t) { - const labels = { - 'natural=peak': '⛰ Вершина', - 'natural=water': '💧 Вода', - 'tourism=viewpoint': '👁 Смотровая', - 'historic=ruins': '🏚 Руины', - 'natural=cave_entrance': '🕳 Пещера', - 'ford=yes': '🌊 Брод', - }; - return labels[t] || t; -} - -// Клик по грунтовкам -['trails-grade12', 'trails-grade345', 'trails-path-bridleway', 'trails-asphalt'].forEach(layerId => { - map.on('click', layerId, (e) => { - const props = e.features[0].properties; - const html = ` - - - - - - ${props.mtb_scale ? `` : ''} - `; - popup.setLngLat(e.lngLat).setHTML(html).addTo(map); - }); - - map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; }); - map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; }); -}); - -// Клик по POI -map.on('click', 'poi-circles', (e) => { - const props = e.features[0].properties; - const html = ` - - - - `; - popup.setLngLat(e.lngLat).setHTML(html).addTo(map); -}); - -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-grade12', 'trails-grade345', 'trails-path-bridleway', - 'trails-asphalt', 'poi-circles'], - }); - if (!features.length) popup.remove(); -}); +initMap(); +