auto-sync: 2026-05-04 00:20:02

This commit is contained in:
Stream
2026-05-04 00:20:02 +03:00
parent 989fc3537a
commit 0a46b5347a
3 changed files with 244 additions and 0 deletions

View File

@@ -274,6 +274,69 @@ body {
animation: location-pulse 1.5s ease-out infinite;
}
/* ─── Поиск (Nominatim) ─────────────────────────────────────────────── */
#search-box {
position: relative;
margin-left: 12px;
}
#search-input {
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 20px;
font-size: 13px;
width: 220px;
outline: none;
background: rgba(255,255,255,0.95);
}
#search-input:focus {
border-color: #e07b00;
box-shadow: 0 0 0 2px rgba(224,123,0,0.15);
}
#search-results {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 300px;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
max-height: 280px;
overflow-y: auto;
}
.search-result-item {
padding: 8px 12px;
cursor: pointer;
font-size: 13px;
border-bottom: 1px solid #f0f0f0;
line-height: 1.4;
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item:hover {
background: #fff8f0;
}
.search-result-name {
font-weight: 600;
color: #333;
}
.search-result-detail {
font-size: 11px;
color: #888;
margin-top: 2px;
}
@keyframes location-pulse {
0% { transform: translate(-50%, -50%) scale(0.5); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1.5); opacity: 0; }

View File

@@ -112,6 +112,8 @@ async function initMap() {
document.getElementById('loading').classList.remove('visible');
checkDataAvailability();
initRouteClicks(map);
initSearch();
initRulerClicks(map);
});
map.on('error', (e) => {
@@ -310,4 +312,178 @@ function initRouteClicks(map) {
});
}
// ─── Поиск (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;
}
searchTimeout = setTimeout(() => doSearch(q), 400);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
results.style.display = 'none';
input.blur();
}
});
document.addEventListener('click', (e) => {
if (!e.target.closest('#search-box')) {
results.style.display = 'none';
}
});
}
async function doSearch(query) {
const results = document.getElementById('search-results');
results.innerHTML = '<div class="search-result-item" style="color:#888">Поиск...</div>';
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 = '<div class="search-result-item" style="color:#888">Ничего не найдено</div>';
return;
}
results.innerHTML = data.map((item, i) => {
const name = item.display_name.split(',')[0];
const detail = item.display_name.split(',').slice(1, 3).join(',').trim();
return `<div class="search-result-item" onclick="selectSearchResult(${item.lat}, ${item.lon}, '${name.replace(/'/g, "\\'")}')">
<div class="search-result-name">${name}</div>
<div class="search-result-detail">${detail}</div>
</div>`;
}).join('');
} catch(e) {
results.innerHTML = '<div class="search-result-item" style="color:#c00">Ошибка поиска</div>';
}
}
function selectSearchResult(lat, lon, name) {
const map = window._map;
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 = [];
let rulerTotal = 0;
function toggleRuler() {
rulerMode = !rulerMode;
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();
}
}
function clearRuler() {
rulerPoints = [];
rulerTotal = 0;
rulerMarkers.forEach(m => m.remove());
rulerMarkers = [];
const map = window._map;
if (map.getLayer('ruler-line')) map.removeLayer('ruler-line');
if (map.getSource('ruler')) map.removeSource('ruler');
}
function haversineKm(a, b) {
const R = 6371;
const dLat = (b[1] - a[1]) * Math.PI / 180;
const dLon = (b[0] - a[0]) * Math.PI / 180;
const s = Math.sin(dLat/2)**2 + Math.cos(a[1]*Math.PI/180) * Math.cos(b[1]*Math.PI/180) * Math.sin(dLon/2)**2;
return R * 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1-s));
}
function updateRulerLine() {
const map = window._map;
const geojson = { type: 'Feature', geometry: { type: 'LineString', coordinates: rulerPoints } };
if (map.getSource('ruler')) {
map.getSource('ruler').setData(geojson);
} 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,
}
});
}
}
function addRulerPoint(lngLat, isLast) {
const map = window._map;
const pt = [lngLat.lng, lngLat.lat];
rulerPoints.push(pt);
// Считаем расстояние от предыдущей точки
let segDist = 0;
if (rulerPoints.length > 1) {
segDist = haversineKm(rulerPoints[rulerPoints.length - 2], pt);
rulerTotal += segDist;
}
// Маркер с подписью
const el = document.createElement('div');
el.style.cssText = 'background:#0088ff;border:2px solid #fff;border-radius:50%;width:10px;height:10px;box-shadow:0 0 4px rgba(0,0,0,0.3)';
// Подпись с накопленным расстоянием
const label = rulerPoints.length === 1 ? '0' :
rulerTotal >= 1 ? rulerTotal.toFixed(1) + ' км' : Math.round(rulerTotal * 1000) + ' м';
const popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false })
.setHTML(`<span style="font-size:12px;font-weight:600;color:#0088ff">${label}</span>`);
const marker = new maplibregl.Marker({ element: el })
.setLngLat([lngLat.lng, lngLat.lat])
.setPopup(popup)
.addTo(map);
marker.togglePopup();
rulerMarkers.push(marker);
updateRulerLine();
}
function initRulerClicks(map) {
map.on('click', (e) => {
if (!rulerMode) return;
addRulerPoint(e.lngLat, false);
});
map.on('dblclick', (e) => {
if (!rulerMode) return;
e.preventDefault();
// Двойной клик = завершить измерение
toggleRuler();
});
}
initMap();

View File

@@ -32,6 +32,10 @@
🗺 Подложка
</button>
</div>
<div id="search-box">
<input type="text" id="search-input" placeholder="🔍 Поиск места..." autocomplete="off" />
<div id="search-results"></div>
</div>
</div>
<div id="map-container">
@@ -104,6 +108,7 @@
<button id="btn-compass" class="map-ctrl-btn" title="Свободное вращение" onclick="toggleCompass()">🧭</button>
<button id="btn-route" class="map-ctrl-btn" title="Построить маршрут" onclick="toggleRouteMode()">🗺️</button>
<button id="btn-locate" class="map-ctrl-btn" title="Моё местоположение" onclick="locateMe()">📍</button>
<button id="btn-ruler" class="map-ctrl-btn" title="Измерить расстояние" onclick="toggleRuler()">📏</button>
</div>
</div>