auto-sync: 2026-05-02 12:40:01
This commit is contained in:
@@ -339,39 +339,116 @@ function toggleLayer(group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Map init ─────────────────────────────────────────────────────────────────
|
// ─── Map init ─────────────────────────────────────────────────────────────────
|
||||||
const map = new maplibregl.Map({
|
async function initMap() {
|
||||||
container: 'map',
|
const tileBase = window.location.origin;
|
||||||
style: '/style.json',
|
const style = await fetch('/style.json').then(r => r.json());
|
||||||
center: [40.5, 55.5],
|
style.sources['trails-tiles'].tiles = [`${tileBase}/api/tiles/{z}/{x}/{y}.mvt`];
|
||||||
zoom: 7,
|
|
||||||
minZoom: 4,
|
|
||||||
maxZoom: 18,
|
|
||||||
hash: true,
|
|
||||||
});
|
|
||||||
window._map = map;
|
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
const map = new maplibregl.Map({
|
||||||
map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right');
|
container: 'map',
|
||||||
map.addControl(new maplibregl.FullscreenControl(), 'top-left');
|
style: style,
|
||||||
|
center: [40.5, 55.5],
|
||||||
|
zoom: 7,
|
||||||
|
minZoom: 4,
|
||||||
|
maxZoom: 18,
|
||||||
|
hash: true,
|
||||||
|
});
|
||||||
|
window._map = map;
|
||||||
|
|
||||||
// ─── Loading state ────────────────────────────────────────────────────────────
|
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
||||||
map.on('load', () => {
|
map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right');
|
||||||
document.getElementById('loading').classList.remove('visible');
|
map.addControl(new maplibregl.FullscreenControl(), 'top-left');
|
||||||
checkDataAvailability();
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('error', (e) => {
|
// ─── Loading state ─────────────────────────────────────────────────────────────────
|
||||||
console.error('Map error:', e.error?.message || e);
|
map.on('load', () => {
|
||||||
// Снимаем loading даже при ошибке чтобы не висело
|
document.getElementById('loading').classList.remove('visible');
|
||||||
document.getElementById('loading').classList.remove('visible');
|
checkDataAvailability();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Таймаут — если load не сработал за 15 сек, снимаем loading
|
map.on('error', (e) => {
|
||||||
setTimeout(() => {
|
console.error('Map error:', e.error?.message || e);
|
||||||
document.getElementById('loading').classList.remove('visible');
|
document.getElementById('loading').classList.remove('visible');
|
||||||
}, 15000);
|
});
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="popup-title">${props.name || 'Без названия'}</div>
|
||||||
|
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${props.highway || '—'}</span></div>
|
||||||
|
<div class="popup-row"><span class="popup-key">Покрытие</span><span class="popup-val">${props.surface || '—'}</span></div>
|
||||||
|
<div class="popup-row"><span class="popup-key">Категория</span><span class="popup-val">${props.tracktype || '—'}</span></div>
|
||||||
|
<div class="popup-row"><span class="popup-key">Длина</span><span class="popup-val">${formatLength(props.length_m)}</span></div>
|
||||||
|
${props.mtb_scale ? `<div class="popup-row"><span class="popup-key">MTB scale</span><span class="popup-val">${props.mtb_scale}</span></div>` : ''}
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="popup-title">${props.name || 'Без названия'}</div>
|
||||||
|
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${poiTypeLabel(props.poi_type)}</span></div>
|
||||||
|
`;
|
||||||
|
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() {
|
async function checkDataAvailability() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/health');
|
const resp = await fetch('/api/health');
|
||||||
@@ -384,83 +461,8 @@ async function checkDataAvailability() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Stats bar ────────────────────────────────────────────────────────────────
|
initMap();
|
||||||
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 = `
|
|
||||||
<div class="popup-title">${props.name || 'Без названия'}</div>
|
|
||||||
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${props.highway || '—'}</span></div>
|
|
||||||
<div class="popup-row"><span class="popup-key">Покрытие</span><span class="popup-val">${props.surface || '—'}</span></div>
|
|
||||||
<div class="popup-row"><span class="popup-key">Категория</span><span class="popup-val">${props.tracktype || '—'}</span></div>
|
|
||||||
<div class="popup-row"><span class="popup-key">Длина</span><span class="popup-val">${formatLength(props.length_m)}</span></div>
|
|
||||||
${props.mtb_scale ? `<div class="popup-row"><span class="popup-key">MTB scale</span><span class="popup-val">${props.mtb_scale}</span></div>` : ''}
|
|
||||||
`;
|
|
||||||
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 = `
|
|
||||||
<div class="popup-title">${props.name || 'Без названия'}</div>
|
|
||||||
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${poiTypeLabel(props.poi_type)}</span></div>
|
|
||||||
<div class="popup-row"><span class="popup-key">OSM ID</span><span class="popup-val">${props.osm_id}</span></div>
|
|
||||||
`;
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user