auto-sync: 2026-05-16 20:20:01
This commit is contained in:
@@ -247,7 +247,8 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
#waypoints-list { display: flex; flex-direction: column; margin-bottom: 10px; }
|
||||
.wl-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 0;
|
||||
padding: 8px 4px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
}
|
||||
@@ -769,3 +770,202 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Terrain Layer (Phase 5.4)
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
/* Terrain toggle button active state */
|
||||
#terrain-toggle.active {
|
||||
color: var(--accent, #4CAF50);
|
||||
background: rgba(76, 175, 80, 0.15);
|
||||
}
|
||||
|
||||
/* Terrain popup */
|
||||
.terrain-popup {
|
||||
position: fixed;
|
||||
z-index: 500;
|
||||
background: var(--surface, #1e1e1e);
|
||||
border: 1px solid var(--border, rgba(255,255,255,0.12));
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.terrain-popup-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text2, rgba(255,255,255,0.5));
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.terrain-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 4px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
color: var(--text, #fff);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.terrain-checkbox span {
|
||||
font-size: 15px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.terrain-checkbox:hover {
|
||||
color: var(--accent, #4CAF50);
|
||||
}
|
||||
|
||||
.terrain-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent, #4CAF50);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Light theme overrides */
|
||||
.theme-light .terrain-popup {
|
||||
background: var(--surface, #fff);
|
||||
border-color: var(--border, rgba(0,0,0,0.12));
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.theme-light .terrain-popup-title {
|
||||
color: var(--text2, rgba(0,0,0,0.5));
|
||||
}
|
||||
|
||||
.theme-light .terrain-checkbox {
|
||||
color: var(--text, #111);
|
||||
}
|
||||
|
||||
|
||||
/* Terrain hillshade hint & disabled state */
|
||||
.terrain-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--accent, #4CAF50);
|
||||
font-style: italic;
|
||||
padding: 4px 0 2px 28px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.terrain-checkbox.disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.terrain-checkbox.disabled input[type="checkbox"] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Scale + Zoom bar (one line, top-right) ───────── */
|
||||
#scale-zoom-bar {
|
||||
position: absolute;
|
||||
top: calc(max(env(safe-area-inset-top, 0px), 8px) + 4px);
|
||||
right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.szb-scale {
|
||||
height: 16px;
|
||||
border: 1.5px solid rgba(255,255,255,0.8);
|
||||
border-top: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.szb-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: rgba(255,255,255,0.9);
|
||||
text-shadow: 0 0 3px rgba(0,0,0,0.8), 0 1px 2px rgba(0,0,0,0.6);
|
||||
white-space: nowrap;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.szb-zoom {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.85);
|
||||
text-shadow: 0 0 3px rgba(0,0,0,0.8), 0 1px 2px rgba(0,0,0,0.6);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
/* ── Search panel ───────────────────────────── */
|
||||
#search-panel {
|
||||
position: fixed;
|
||||
bottom: calc(68px + env(safe-area-inset-bottom, 0px));
|
||||
left: 0; right: 0;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
z-index: 350;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
.search-panel-inner {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
#standalone-search-input {
|
||||
flex: 1;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 15px;
|
||||
color: var(--text1);
|
||||
outline: none;
|
||||
}
|
||||
#standalone-search-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
#search-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text3);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
#standalone-search-results {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
margin-top: 8px;
|
||||
}
|
||||
#standalone-search-results .search-result-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
#standalone-search-results .search-result-item:hover,
|
||||
#standalone-search-results .search-result-item:active {
|
||||
background: var(--surface2);
|
||||
}
|
||||
#standalone-search-results .search-result-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text1);
|
||||
}
|
||||
#standalone-search-results .search-result-sub {
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
|
||||
223
tasks/enduro-trails/prototype/static/app.js
vendored
223
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -99,9 +99,10 @@ function switchMapStyle() {
|
||||
fetch(styleUrl, { method: 'HEAD' }).then(r => {
|
||||
if (r.ok) {
|
||||
map.setStyle(styleUrl);
|
||||
// Restore position after style loads
|
||||
map.once('style.load', () => {
|
||||
// Restore position and overlays after style loads
|
||||
map.once('idle', () => {
|
||||
map.jumpTo({ center, zoom, bearing, pitch });
|
||||
rebuildMapOverlays();
|
||||
});
|
||||
} else {
|
||||
console.log('Map style not available:', styleUrl);
|
||||
@@ -120,6 +121,10 @@ function onMapStyleLoad() {
|
||||
}
|
||||
|
||||
function rebuildMapOverlays() {
|
||||
// Re-apply terrain and trails after style change
|
||||
restoreTerrainState();
|
||||
restoreTrailsState();
|
||||
|
||||
// Re-apply recon circle if active
|
||||
if (reconMode && reconCenter) {
|
||||
doRecon(reconCenter[0], reconCenter[1]);
|
||||
@@ -1283,9 +1288,7 @@ function selectMarkerType(idx, lat, lng) {
|
||||
closeMarkerDialog();
|
||||
const markers = loadMarkers();
|
||||
const icon = MARKER_ICONS[idx] || MARKER_ICONS[0];
|
||||
const name = prompt('Название метки (Enter = автоимя):');
|
||||
if (name === null) return;
|
||||
const autoName = name.trim() || `Метка ${markers.length + 1}`;
|
||||
const autoName = `Метка ${markers.length + 1}`;
|
||||
const marker = { id: Date.now(), name: autoName, icon, lat, lon: lng };
|
||||
markers.push(marker);
|
||||
saveMarkers(markers);
|
||||
@@ -1320,6 +1323,17 @@ function drawNamedMarker(markerData) {
|
||||
}
|
||||
|
||||
function renderMarkers() {
|
||||
// Clear existing marker objects to prevent duplicates
|
||||
Object.keys(namedMarkerObjects).forEach(id => {
|
||||
const obj = namedMarkerObjects[id];
|
||||
if (obj) {
|
||||
const popup = obj.getPopup();
|
||||
if (popup) popup.remove();
|
||||
obj.remove();
|
||||
}
|
||||
});
|
||||
namedMarkerObjects = {};
|
||||
// Re-draw from localStorage
|
||||
const markers = loadMarkers();
|
||||
markers.forEach(m => drawNamedMarker(m));
|
||||
}
|
||||
@@ -1379,8 +1393,52 @@ async function initMap() {
|
||||
window._map = map;
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
||||
map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right');
|
||||
map.addControl(new maplibregl.FullscreenControl(), 'top-left');
|
||||
// Custom scale + zoom indicator (one line, top-right)
|
||||
const scaleZoomBar = document.createElement('div');
|
||||
scaleZoomBar.id = 'scale-zoom-bar';
|
||||
scaleZoomBar.innerHTML = '<div class="szb-scale"><span class="szb-label">30 km</span></div><div class="szb-zoom">z7</div>';
|
||||
document.getElementById('map').appendChild(scaleZoomBar);
|
||||
|
||||
function updateScaleZoom() {
|
||||
const zoom = Math.round(map.getZoom());
|
||||
const lat = map.getCenter().lat;
|
||||
const metersPerPixel = 156543.03392 * Math.cos(lat * Math.PI / 180) / Math.pow(2, map.getZoom());
|
||||
|
||||
const targetPx = 80;
|
||||
const rawMeters = metersPerPixel * targetPx;
|
||||
|
||||
let distance, unit, niceMeters;
|
||||
if (rawMeters >= 1000) {
|
||||
const km = rawMeters / 1000;
|
||||
distance = km >= 100 ? Math.round(km / 50) * 50 :
|
||||
km >= 10 ? Math.round(km / 5) * 5 :
|
||||
km >= 1 ? Math.round(km) : Math.round(km * 10) / 10;
|
||||
unit = 'km';
|
||||
niceMeters = distance * 1000;
|
||||
} else {
|
||||
distance = rawMeters >= 100 ? Math.round(rawMeters / 50) * 50 :
|
||||
rawMeters >= 10 ? Math.round(rawMeters / 5) * 5 :
|
||||
Math.round(rawMeters);
|
||||
unit = 'm';
|
||||
niceMeters = distance;
|
||||
}
|
||||
|
||||
const actualPx = Math.round(niceMeters / metersPerPixel);
|
||||
const clampedPx = Math.max(40, Math.min(150, actualPx));
|
||||
|
||||
const scaleEl = scaleZoomBar.querySelector('.szb-scale');
|
||||
const labelEl = scaleZoomBar.querySelector('.szb-label');
|
||||
const zoomEl = scaleZoomBar.querySelector('.szb-zoom');
|
||||
|
||||
scaleEl.style.width = clampedPx + 'px';
|
||||
labelEl.textContent = distance + ' ' + unit;
|
||||
zoomEl.textContent = 'z' + zoom;
|
||||
}
|
||||
|
||||
updateScaleZoom();
|
||||
map.on('zoom', updateScaleZoom);
|
||||
map.on('move', updateScaleZoom);
|
||||
|
||||
map.on('load', () => {
|
||||
checkDataAvailability();
|
||||
@@ -2637,16 +2695,25 @@ function toggleTerrainPopup() {
|
||||
|
||||
const isVisible = popup.style.display !== 'none';
|
||||
popup.style.display = isVisible ? 'none' : 'block';
|
||||
btn.classList.toggle('active', !isVisible);
|
||||
|
||||
// Close on outside click
|
||||
// Position popup to the left of the button
|
||||
if (!isVisible) {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
popup.style.right = (window.innerWidth - rect.left + 8) + 'px';
|
||||
// Position: align bottom of popup with bottom of button, ensure fits in viewport
|
||||
const popupHeight = popup.offsetHeight;
|
||||
const desiredTop = rect.bottom - popupHeight;
|
||||
const minTop = 8;
|
||||
popup.style.top = Math.max(minTop, desiredTop) + 'px';
|
||||
updateHillshadeAvailability();
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeTerrainOnOutside);
|
||||
}, 10);
|
||||
} else {
|
||||
document.removeEventListener('click', closeTerrainOnOutside);
|
||||
}
|
||||
|
||||
btn.classList.toggle('active', !isVisible);
|
||||
}
|
||||
|
||||
function closeTerrainOnOutside(e) {
|
||||
@@ -2663,20 +2730,66 @@ function onTerrainCheckbox() {
|
||||
const map = window._map;
|
||||
if (!map) return;
|
||||
|
||||
const hypsoChecked = document.getElementById('terrain-hypso-cb').checked;
|
||||
const hillshadeChecked = document.getElementById('terrain-hillshade-cb').checked;
|
||||
const triChecked = document.getElementById('terrain-tri-cb').checked;
|
||||
|
||||
// Save state
|
||||
localStorage.setItem('terrain-hypso', hypsoChecked ? '1' : '0');
|
||||
localStorage.setItem('terrain-hillshade', hillshadeChecked ? '1' : '0');
|
||||
localStorage.setItem('terrain-tri', triChecked ? '1' : '0');
|
||||
|
||||
// Update button active state
|
||||
const btn = document.getElementById('terrain-toggle');
|
||||
btn.classList.toggle('active', hypsoChecked || hillshadeChecked);
|
||||
btn.classList.toggle('active', hillshadeChecked || triChecked);
|
||||
|
||||
// Apply layers
|
||||
applyTerrainLayer('terrain-hypso', TERRAIN_BASE_URL + '/hypso/{z}/{x}/{y}.png', hypsoChecked, 0.55, 5, 15);
|
||||
applyTerrainLayer('terrain-hillshade', TERRAIN_BASE_URL + '/hillshade/{z}/{x}/{y}.png', hillshadeChecked, 0.40, 10, 15);
|
||||
applyTerrainLayer('terrain-tri', TERRAIN_BASE_URL + '/tri/{z}/{x}/{y}.png', triChecked, 0.70, 5, 15);
|
||||
}
|
||||
|
||||
|
||||
function onTrailsCheckbox() {
|
||||
const map = window._map;
|
||||
if (!map) return;
|
||||
|
||||
const trackChecked = document.getElementById('trails-track-cb').checked;
|
||||
const pathChecked = document.getElementById('trails-path-cb').checked;
|
||||
|
||||
// Save state
|
||||
localStorage.setItem('trails-track', trackChecked ? '1' : '0');
|
||||
localStorage.setItem('trails-path', pathChecked ? '1' : '0');
|
||||
|
||||
// Toggle layer visibility
|
||||
if (map.getLayer('trails-track')) {
|
||||
map.setLayoutProperty('trails-track', 'visibility', trackChecked ? 'visible' : 'none');
|
||||
}
|
||||
if (map.getLayer('trails-path-bridleway')) {
|
||||
map.setLayoutProperty('trails-path-bridleway', 'visibility', pathChecked ? 'visible' : 'none');
|
||||
}
|
||||
}
|
||||
|
||||
function restoreTrailsState() {
|
||||
const trackState = localStorage.getItem('trails-track');
|
||||
const pathState = localStorage.getItem('trails-path');
|
||||
|
||||
// Default: both checked (visible)
|
||||
const trackOn = trackState === null || trackState === '1';
|
||||
const pathOn = pathState === null || pathState === '1';
|
||||
|
||||
const trackCb = document.getElementById('trails-track-cb');
|
||||
const pathCb = document.getElementById('trails-path-cb');
|
||||
|
||||
if (trackCb) trackCb.checked = trackOn;
|
||||
if (pathCb) pathCb.checked = pathOn;
|
||||
|
||||
const map = window._map;
|
||||
if (map) {
|
||||
if (map.getLayer('trails-track')) {
|
||||
map.setLayoutProperty('trails-track', 'visibility', trackOn ? 'visible' : 'none');
|
||||
}
|
||||
if (map.getLayer('trails-path-bridleway')) {
|
||||
map.setLayoutProperty('trails-path-bridleway', 'visibility', pathOn ? 'visible' : 'none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
|
||||
@@ -2693,7 +2806,6 @@ function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
|
||||
tiles: [tileUrl],
|
||||
tileSize: 256,
|
||||
scheme: 'tms',
|
||||
bounds: [35, 45, 55, 62],
|
||||
minzoom: minzoom,
|
||||
maxzoom: maxzoom
|
||||
});
|
||||
@@ -2709,7 +2821,8 @@ function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
|
||||
type: 'raster',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'raster-opacity': opacity
|
||||
'raster-opacity': opacity,
|
||||
'raster-resampling': 'linear'
|
||||
},
|
||||
minzoom: minzoom,
|
||||
maxzoom: maxzoom
|
||||
@@ -2743,22 +2856,22 @@ function updateHillshadeAvailability() {
|
||||
}
|
||||
|
||||
function restoreTerrainState() {
|
||||
const hypso = localStorage.getItem('terrain-hypso') === '1';
|
||||
const hillshade = localStorage.getItem('terrain-hillshade') === '1';
|
||||
const tri = localStorage.getItem('terrain-tri') === '1';
|
||||
|
||||
const hypsoCb = document.getElementById('terrain-hypso-cb');
|
||||
const hillshadeCb = document.getElementById('terrain-hillshade-cb');
|
||||
const triCb = document.getElementById('terrain-tri-cb');
|
||||
|
||||
if (hypsoCb) hypsoCb.checked = hypso;
|
||||
if (hillshadeCb) hillshadeCb.checked = hillshade;
|
||||
if (triCb) triCb.checked = tri;
|
||||
|
||||
if (hypso || hillshade) {
|
||||
if (hillshade || tri) {
|
||||
onTerrainCheckbox();
|
||||
}
|
||||
|
||||
// Update button active state
|
||||
const btn = document.getElementById('terrain-toggle');
|
||||
if (btn) btn.classList.toggle('active', hypso || hillshade);
|
||||
if (btn) btn.classList.toggle('active', hillshade || tri);
|
||||
}
|
||||
|
||||
// Hook into map load and zoom changes
|
||||
@@ -2771,8 +2884,8 @@ function restoreTerrainState() {
|
||||
setTimeout(restoreTerrainState, 100);
|
||||
});
|
||||
// Initial state
|
||||
updateHillshadeAvailability();
|
||||
restoreTerrainState();
|
||||
restoreTrailsState();
|
||||
} else {
|
||||
// Map not ready yet, wait
|
||||
const interval = setInterval(() => {
|
||||
@@ -2784,7 +2897,77 @@ function restoreTerrainState() {
|
||||
});
|
||||
updateHillshadeAvailability();
|
||||
restoreTerrainState();
|
||||
restoreTrailsState();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
})();
|
||||
|
||||
// ─── Standalone Search Mode ──────────────────────────────────────
|
||||
let searchModeActive = false;
|
||||
let standaloneSearchTimeout = null;
|
||||
|
||||
function toggleSearchMode() {
|
||||
searchModeActive = !searchModeActive;
|
||||
const panel = document.getElementById('search-panel');
|
||||
const btn = document.getElementById('tb-search');
|
||||
|
||||
if (searchModeActive) {
|
||||
panel.style.display = 'block';
|
||||
btn.classList.add('active');
|
||||
const input = document.getElementById('standalone-search-input');
|
||||
input.value = '';
|
||||
document.getElementById('standalone-search-results').innerHTML = '';
|
||||
setTimeout(() => input.focus(), 100);
|
||||
|
||||
input.oninput = () => {
|
||||
clearTimeout(standaloneSearchTimeout);
|
||||
const q = input.value.trim();
|
||||
if (q.length < 2) {
|
||||
document.getElementById('standalone-search-results').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
standaloneSearchTimeout = setTimeout(() => doStandaloneSearch(q), 400);
|
||||
};
|
||||
|
||||
input.onkeydown = (e) => {
|
||||
if (e.key === 'Escape') toggleSearchMode();
|
||||
};
|
||||
} else {
|
||||
panel.style.display = 'none';
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
async function doStandaloneSearch(query) {
|
||||
const resultsEl = document.getElementById('standalone-search-results');
|
||||
resultsEl.innerHTML = '<div class="search-result-item"><span style="color:var(--text3)">Поиск...</span></div>';
|
||||
|
||||
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);
|
||||
const data = await resp.json();
|
||||
|
||||
if (!data.length) {
|
||||
resultsEl.innerHTML = '<div class="search-result-item"><span style="color:var(--text3)">Ничего не найдено</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
resultsEl.innerHTML = data.map(item => {
|
||||
const parts = item.display_name.split(',');
|
||||
const name = parts[0].trim();
|
||||
const sub = parts.slice(1, 3).join(',').trim();
|
||||
return `<div class="search-result-item" onclick="standaloneSelectResult(${item.lat}, ${item.lon}, '${name.replace(/'/g, "\\\'")}')">
|
||||
<div class="search-result-name">${name}</div>
|
||||
${sub ? `<div class="search-result-sub">${sub}</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch(e) {
|
||||
resultsEl.innerHTML = '<div class="search-result-item"><span style="color:var(--red)">Ошибка поиска</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function standaloneSelectResult(lat, lon, name) {
|
||||
toggleSearchMode();
|
||||
window._map.flyTo({ center: [parseFloat(lon), parseFloat(lat)], zoom: 13, duration: 800 });
|
||||
}
|
||||
|
||||
@@ -32,6 +32,29 @@
|
||||
<!-- ── No data warning ───────────────────── -->
|
||||
<div id="no-data-warning">⚠️ База данных недоступна</div>
|
||||
|
||||
<!-- ── Terrain popup ────────────────────── -->
|
||||
<div id="terrain-popup" class="terrain-popup" style="display:none">
|
||||
<div class="terrain-popup-title">Эндуро</div>
|
||||
<label class="terrain-checkbox">
|
||||
<input type="checkbox" id="terrain-hillshade-cb" onchange="onTerrainCheckbox()">
|
||||
<span>Тени рельефа</span>
|
||||
</label>
|
||||
<span class="terrain-hint" id="terrain-hillshade-hint" style="display:none">Зум 10+</span>
|
||||
<label class="terrain-checkbox">
|
||||
<input type="checkbox" id="terrain-tri-cb" onchange="onTerrainCheckbox()">
|
||||
<span>Перепады</span>
|
||||
</label>
|
||||
<hr style="margin:6px 0;border-color:rgba(128,128,128,0.3)">
|
||||
<label class="terrain-checkbox">
|
||||
<input type="checkbox" id="trails-track-cb" onchange="onTrailsCheckbox()" checked>
|
||||
<span>Грунтовки</span>
|
||||
</label>
|
||||
<label class="terrain-checkbox">
|
||||
<input type="checkbox" id="trails-path-cb" onchange="onTrailsCheckbox()" checked>
|
||||
<span>Тропы</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- ── Map Buttons (right) ───────────────── -->
|
||||
<div id="map-controls-r">
|
||||
<button class="map-btn" id="btn-compass" onclick="toggleCompass()" title="Компас">
|
||||
@@ -40,6 +63,9 @@
|
||||
<button class="map-btn" onclick="locateMe()" title="Моё местоположение">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg>
|
||||
</button>
|
||||
<button class="map-btn" id="terrain-toggle" onclick="toggleTerrainPopup()" title="Эндуро">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/><path d="M12 3v6M12 15v6M3 12h6M15 12h6M5.6 5.6l4.3 4.3M14.1 14.1l4.3 4.3M5.6 18.4l4.3-4.3M14.1 9.9l4.3-4.3"/></svg>
|
||||
</button>
|
||||
<button class="map-btn" id="btn-theme" onclick="toggleTheme()" title="Переключить тему">
|
||||
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
||||
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
||||
@@ -197,6 +223,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search panel -->
|
||||
<div id="search-panel" style="display:none">
|
||||
<div class="search-panel-inner">
|
||||
<input id="standalone-search-input" type="text" placeholder="Поиск места..." autocomplete="off" autocorrect="off">
|
||||
<button id="search-close-btn" onclick="toggleSearchMode()">✕</button>
|
||||
</div>
|
||||
<div id="standalone-search-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
BOTTOM TOOLBAR
|
||||
════════════════════════════════════════════ -->
|
||||
@@ -221,29 +256,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"/><path d="m14.5 12.5 2-2"/><path d="m11.5 9.5 2-2"/><path d="m8.5 6.5 2-2"/><path d="m17.5 15.5 2-2"/></svg>
|
||||
<span>Линейка</span>
|
||||
</button>
|
||||
<button class="tb-btn" id="tb-search" onclick="toggleSearchMode()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<span>Поиск</span>
|
||||
</button>
|
||||
<button class="tb-btn" id="tb-marker" onclick="toggleMarkerMode()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
<span>Метка</span>
|
||||
</button>
|
||||
<button class="tb-btn" id="terrain-toggle" onclick="toggleTerrainPopup()" title="Рельеф">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></svg>
|
||||
<span>Рельеф</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Terrain popup -->
|
||||
<div id="terrain-popup" style="display:none; position:fixed; bottom:80px; right:12px; background:var(--surface); border-radius:12px; padding:12px 16px; box-shadow:0 4px 20px rgba(0,0,0,0.3); z-index:1000; min-width:180px;">
|
||||
<div style="font-weight:600; margin-bottom:10px; font-size:14px;">🏔️ Рельеф</div>
|
||||
<label style="display:flex; align-items:center; gap:8px; margin-bottom:8px; cursor:pointer; font-size:14px;">
|
||||
<input type="checkbox" id="terrain-hypso-cb" onchange="onTerrainCheckbox()">
|
||||
Гипсометрия
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:14px;">
|
||||
<input type="checkbox" id="terrain-hillshade-cb" onchange="onTerrainCheckbox()">
|
||||
<span id="terrain-hillshade-label">Отмывка</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Mini route sheet -->
|
||||
<div id="sheet-route-mini">
|
||||
<div class="mini-handle" id="mini-route-handle"></div>
|
||||
|
||||
Reference in New Issue
Block a user