auto-sync: 2026-05-02 12:40:01

This commit is contained in:
Stream
2026-05-02 12:40:01 +03:00
parent bfcf5a51f2
commit 494678191b

View File

@@ -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>