auto-sync: 2026-05-02 12:40:01
This commit is contained in:
@@ -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 = `
|
||||
<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() {
|
||||
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 = `
|
||||
<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();
|
||||
});
|
||||
initMap();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user