Клиентская загрузка GPX 1.1: парсинг через DOMParser с чанковой конвертацией (ADR-003), отрисовка треков и waypoints на карте, панель #sheet-gpx со списком треков, статистикой и canvas-профилем высот. GPX-слои встают ниже маршрута OSRM и восстанавливаются после смены стиля карты (REQ-F-13). - src/web/gpx.js — новый модуль фичи (ADR-002): парсинг, модель window.gpxTracks, слои/маркеры карты, sheet-gpx, профиль высот - index.html / app.css — кнопка загрузки, кнопка тулбара, панель #sheet-gpx, toast-уведомления, индикатор парсинга - app.js — один хук rebuildGpxOverlays() в rebuildMapOverlays() - тесты: gpx.test.js (node --test, U-01..U-21) + test_gpx_upload.py (pytest: статические проверки + JS-раннер) Refs: ET-006 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
465 lines
34 KiB
HTML
465 lines
34 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<title>Enduro Trails</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.css">
|
||
<link rel="stylesheet" href="app.css">
|
||
</head>
|
||
<body class="theme-dark">
|
||
|
||
<!-- Map -->
|
||
<div id="map"></div>
|
||
|
||
<!-- Sheet backdrop -->
|
||
<div id="sheet-backdrop" onclick="closeAllSheets()"></div>
|
||
|
||
|
||
|
||
<!-- ── Ruler info ─────────────────────────── -->
|
||
<div id="ruler-info">
|
||
<span id="ruler-dist">0 км</span>
|
||
<button class="ruler-action-btn" onclick="exitRulerMode()" title="Завершить">✓ Завершить</button>
|
||
<button class="ruler-action-btn ruler-action-btn--danger" onclick="deleteRuler()" title="Удалить линейку">✕</button>
|
||
</div>
|
||
<!-- ── Ruler toast hint ───────────────────── -->
|
||
<div id="ruler-toast">Тапни на карту чтобы добавить точку</div>
|
||
|
||
<!-- ── No data warning ───────────────────── -->
|
||
<div id="no-data-warning">⚠️ База данных недоступна</div>
|
||
|
||
<!-- ── ET-006: toast-уведомления и индикатор парсинга GPX ─── -->
|
||
<div id="app-toast"></div>
|
||
<div id="gpx-loading">
|
||
<div class="gpx-spinner"></div>
|
||
<span>Читаю GPX…</span>
|
||
</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>
|
||
<hr style="margin:6px 0;border-color:rgba(128,128,128,0.3)">
|
||
<label class="terrain-checkbox">
|
||
<input type="checkbox" id="poi-visible-cb" onchange="onPoiCheckbox()" checked>
|
||
<span>POI</span>
|
||
</label>
|
||
<hr style="margin:6px 0;border-color:rgba(128,128,128,0.3)">
|
||
<!-- ET-005: переключатель единиц измерения расстояний (км/мили) -->
|
||
<div class="terrain-unit-row">
|
||
<span class="terrain-unit-label">Единицы</span>
|
||
<div class="seg-control unit-seg" id="unit-seg">
|
||
<button type="button" class="seg-btn" id="unit-btn-km" data-unit="km" onclick="onUnitToggle('km')">км</button>
|
||
<button type="button" class="seg-btn" id="unit-btn-mi" data-unit="mi" onclick="onUnitToggle('mi')">мили</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Map Buttons (right) ───────────────── -->
|
||
<div id="map-controls-r">
|
||
<button class="map-btn" id="btn-compass" onclick="toggleCompass()" 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="10"/><path d="m16.24 7.76-2.12 6.36-6.36 2.12 2.12-6.36 6.36-2.12z"/></svg>
|
||
</button>
|
||
<!-- ET-006: загрузка GPX-треков (TRZ REQ-F-01) -->
|
||
<button class="map-btn" id="btn-gpx-upload" onclick="document.getElementById('gpx-file-input').click()" title="Загрузить GPX">
|
||
<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"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||
</button>
|
||
<input type="file" id="gpx-file-input" accept=".gpx" multiple style="display:none" onchange="onGpxFileSelected(this)">
|
||
<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"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></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>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
BOTTOM SHEETS
|
||
════════════════════════════════════════════ -->
|
||
|
||
<!-- ── Sheet: Маршрут ────────────────────── -->
|
||
<div class="bottom-sheet" id="sheet-route">
|
||
<div class="sheet-handle"></div>
|
||
<div class="sheet-header">
|
||
<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"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"/><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"/><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"/><path d="M14 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-3"/></svg>
|
||
<h2>Маршрут</h2>
|
||
<!-- Скачать GPX -->
|
||
<button class="sheet-icon-btn" onclick="downloadGPX()" title="Скачать GPX">
|
||
<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">
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
||
</svg>
|
||
</button>
|
||
<!-- Сброс -->
|
||
<button class="sheet-icon-btn danger" onclick="resetRouteFromSheet()" 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">
|
||
<path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
||
</svg>
|
||
</button>
|
||
<!-- Свернуть -->
|
||
<button class="sheet-close" onclick="minimizeSheet('sheet-route')" 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">
|
||
<polyline points="6 9 12 15 18 9"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="sheet-body">
|
||
<div id="waypoints-list"></div>
|
||
|
||
<div id="route-status" class="text-muted">Тапни точку старта на карте</div>
|
||
<div id="route-cards"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Sheet: Разведка ───────────────────── -->
|
||
<div class="bottom-sheet" id="sheet-recon">
|
||
<div class="sheet-handle"></div>
|
||
<div class="sheet-header">
|
||
<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"><path d="M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4"/><path d="M5 19.5C5.5 18 6 15 6 12c0-.7.12-1.37.34-2"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M8.65 22c.21-.66.45-1.32.57-2"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M2 16h.01"/><path d="M21.8 16c.2-2 .131-5.354 0-6"/><path d="M9 6.8a6 6 0 0 1 9 5.2c0 .47 0 1.17-.02 2"/></svg>
|
||
<h2>Разведка</h2>
|
||
<button class="sheet-close" onclick="minimizeSheet('sheet-recon')">
|
||
<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"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="sheet-body">
|
||
<p class="sheet-hint" id="recon-hint">Тапни точку на карте — узнаешь сколько грунтовок рядом</p>
|
||
<div class="section-label">РАДИУС</div>
|
||
<div class="seg-control">
|
||
<button class="seg-btn active" data-km="20" onclick="setReconRadius(20)">20 км</button>
|
||
<button class="seg-btn" data-km="50" onclick="setReconRadius(50)">50 км</button>
|
||
<button class="seg-btn" data-km="100" onclick="setReconRadius(100)">100 км</button>
|
||
</div>
|
||
<div id="recon-results" style="display:none">
|
||
<div class="section-label">ГРУНТОВКИ</div>
|
||
<div class="recon-grid">
|
||
<div class="recon-stat">
|
||
<div class="rs-value" id="r-total-km">—</div>
|
||
<div class="rs-label">км всего</div>
|
||
</div>
|
||
<div class="recon-stat">
|
||
<div class="rs-value gold" id="r-lev12-km">—</div>
|
||
<div class="rs-label">км Lev 1-2</div>
|
||
</div>
|
||
<div class="recon-stat">
|
||
<div class="rs-value red" id="r-lev345-km">—</div>
|
||
<div class="rs-label">км Lev 3-5</div>
|
||
</div>
|
||
<div class="recon-stat">
|
||
<div class="rs-value" id="r-path-km">—</div>
|
||
<div class="rs-label">км тропы</div>
|
||
</div>
|
||
</div>
|
||
<div class="section-label">ИНТЕРЕСНОЕ</div>
|
||
<div id="r-poi-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Sheet: Красивый маршрут ───────────── -->
|
||
<div class="bottom-sheet" id="sheet-scenic">
|
||
<div class="sheet-handle"></div>
|
||
<div class="sheet-header">
|
||
<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"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4M19 17v4M3 5h4M17 19h4"/></svg>
|
||
<h2>Красивый маршрут</h2>
|
||
<button class="sheet-close" onclick="minimizeSheet('sheet-scenic')">
|
||
<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"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="sheet-body">
|
||
<div id="scenic-status" class="text-muted">Тапни точку старта на карте</div>
|
||
|
||
<div class="section-label mt-8">ДИСТАНЦИЯ</div>
|
||
<div class="dist-row">
|
||
<div class="seg-control" style="flex:1">
|
||
<button class="seg-btn" data-km="50" onclick="setScenicKm(50)">50</button>
|
||
<button class="seg-btn active" data-km="100" onclick="setScenicKm(100)">100</button>
|
||
<button class="seg-btn" data-km="150" onclick="setScenicKm(150)">150</button>
|
||
<button class="seg-btn" data-km="200" onclick="setScenicKm(200)">200</button>
|
||
</div>
|
||
<input type="number" id="scenic-custom-km" class="dist-custom" placeholder="км" min="20" max="500" onchange="setScenicKm(+this.value||100)">
|
||
</div>
|
||
|
||
<button class="btn-primary" id="btn-build-scenic" onclick="buildScenicRoute()" style="display:none">
|
||
<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"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
||
Построить маршрут
|
||
</button>
|
||
|
||
<div id="scenic-cards" class="mt-8"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Sheet: Связка ─────────────────────── -->
|
||
<div class="bottom-sheet" id="sheet-link">
|
||
<div class="sheet-handle"></div>
|
||
<div class="sheet-header">
|
||
<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"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
|
||
<h2>Связка</h2>
|
||
<button class="sheet-close" onclick="minimizeSheet('sheet-link')">
|
||
<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"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="sheet-body">
|
||
<p class="sheet-hint" style="text-align:left;padding-top:0">Соедини два трека — найду оптимальную грунтовую связку</p>
|
||
<div class="link-points">
|
||
<div class="link-pt empty" id="link-pt-1">
|
||
<div class="link-pt-num">1</div>
|
||
<div class="link-pt-label">Конец первого трека</div>
|
||
</div>
|
||
<div class="link-pt empty" id="link-pt-2">
|
||
<div class="link-pt-num">2</div>
|
||
<div class="link-pt-label">Начало второго трека</div>
|
||
</div>
|
||
</div>
|
||
<div id="link-status" class="text-muted">Тапни первую точку на карте</div>
|
||
<div id="link-cards"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Marker type dialog ─────────────────── -->
|
||
<div id="marker-dialog">
|
||
<div class="marker-dialog-inner">
|
||
<div class="sheet-handle"></div>
|
||
<div style="padding:8px 0 4px;font-size:13px;font-weight:700;color:var(--text)">Тип метки</div>
|
||
<div class="marker-type-grid" id="marker-type-grid"></div>
|
||
<button onclick="closeMarkerDialog()" style="width:100%;height:44px;background:var(--surface2);border:1px solid var(--border);border-radius:12px;color:var(--text2);font-size:14px;font-weight:600;cursor:pointer;margin-top:4px">Отмена</button>
|
||
</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>
|
||
|
||
<!-- ── ET-006: Sheet «GPX-треки» (TRZ REQ-F-09, §3.3) ─────── -->
|
||
<div class="bottom-sheet" id="sheet-gpx">
|
||
<div class="sheet-handle"></div>
|
||
<div class="sheet-header">
|
||
<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"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="m7 17 2.5-4 2 2L15 11"/></svg>
|
||
<h2>GPX-треки</h2>
|
||
<button class="sheet-close" onclick="toggleGpxSheet()" 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"><polyline points="6 9 12 15 18 9"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="sheet-body">
|
||
<p class="sheet-hint" id="gpx-empty">Нажми кнопку загрузки GPX справа на карте, чтобы добавить трек</p>
|
||
<div id="gpx-list"></div>
|
||
<div id="gpx-detail" style="display:none">
|
||
<div class="section-label">Статистика</div>
|
||
<div class="gpx-stats-grid" id="gpx-stats"></div>
|
||
<div class="section-label">Профиль высот</div>
|
||
<div id="gpx-elevation-wrap">
|
||
<canvas id="gpx-elevation-canvas"></canvas>
|
||
<div id="gpx-elevation-tip"></div>
|
||
<p id="gpx-elevation-empty" style="display:none">Данные высот отсутствуют</p>
|
||
</div>
|
||
<div id="gpx-elevation-axis"><span>0 км</span><span></span><span></span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
BOTTOM TOOLBAR
|
||
════════════════════════════════════════════ -->
|
||
<nav id="toolbar">
|
||
<button class="tb-btn" id="tb-route" onclick="toggleRouteMode()">
|
||
<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="M3 3h5l2 9h10l-1.5 7H6.5"/><circle cx="10" cy="20" r="1"/><circle cx="18" cy="20" r="1"/></svg>
|
||
<span>Маршрут</span>
|
||
</button>
|
||
<button class="tb-btn" id="tb-link" onclick="toggleLinkMode()">
|
||
<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"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
|
||
<span>Связка</span>
|
||
</button>
|
||
<button class="tb-btn" id="tb-scenic" onclick="toggleScenicMode()">
|
||
<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="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
||
<span>Красивый</span>
|
||
</button>
|
||
<button class="tb-btn" id="tb-recon" onclick="toggleReconMode()">
|
||
<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="M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4"/><path d="M5 19.5C5.5 18 6 15 6 12c0-.7.12-1.37.34-2"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M8.65 22c.21-.66.45-1.32.57-2"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M2 16h.01"/><path d="M21.8 16c.2-2 .131-5.354 0-6"/><path d="M9 6.8a6 6 0 0 1 9 5.2c0 .47 0 1.17-.02 2"/></svg>
|
||
<span>Разведка</span>
|
||
</button>
|
||
<button class="tb-btn" id="tb-ruler" onclick="toggleRuler()">
|
||
<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>
|
||
<!-- ET-006: переключатель панели GPX-треков (TRZ §3.2) -->
|
||
<button class="tb-btn" id="tb-gpx" onclick="toggleGpxSheet()">
|
||
<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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="m7 17 2.5-4 2 2L15 11"/></svg>
|
||
<span>GPX</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<!-- Mini route sheet -->
|
||
<div id="sheet-route-mini">
|
||
<div class="mini-handle" id="mini-route-handle"></div>
|
||
<div class="mini-route-info">
|
||
<!-- Onboarding panel (shown before both waypoints are set) -->
|
||
<div id="mini-onboard" style="display:none; align-items:center; gap:8px; width:100%;">
|
||
<div id="mini-onboard-pin"></div>
|
||
<span id="mini-onboard-hint" style="flex:1; font-size:13px; color:var(--text2);"></span>
|
||
<button id="mini-onboard-search-btn" style="background:none;border:none;padding:4px 8px;cursor:pointer;color:var(--text2);">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||
</button>
|
||
<!-- Кнопка отмены — показывается только в режиме добавления точки -->
|
||
<button id="mini-onboard-cancel-btn" style="display:none;background:none;border:none;padding:4px 8px;cursor:pointer;color:var(--text3);font-size:20px;line-height:1;" onclick="cancelAddWaypoint()" title="Отмена">✕</button>
|
||
</div>
|
||
<!-- Inline search panel for onboarding -->
|
||
<div id="mini-onboard-search-panel" style="display:none; position:absolute; bottom:100%; left:0; right:0; background:var(--surface); border:1px solid var(--border); border-radius:8px 8px 0 0; padding:8px; z-index:300;">
|
||
<input id="mini-onboard-search-input" type="text" placeholder="Поиск места..." autocomplete="off" autocorrect="off"
|
||
style="width:100%; box-sizing:border-box; padding:8px 10px; border:1px solid var(--border); border-radius:6px; background:var(--surface2); color:var(--text); font-size:14px;">
|
||
<div id="mini-onboard-search-results" style="max-height:200px; overflow-y:auto; margin-top:4px;"></div>
|
||
</div>
|
||
<!-- Moto wheel loading indicator -->
|
||
<svg id="mini-wheel" class="moto-wheel" width="22" height="22" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<!-- Кноблинг: 36 зубцов по внешнему краю -->
|
||
<rect x="92.50" y="47.50" width="3" height="5" fill="currentColor" transform="rotate(0,94.00,50.00)"/>
|
||
<rect x="91.83" y="55.14" width="3" height="5" fill="currentColor" transform="rotate(10,93.33,57.64)"/>
|
||
<rect x="89.85" y="62.55" width="3" height="5" fill="currentColor" transform="rotate(20,91.35,65.05)"/>
|
||
<rect x="86.61" y="69.50" width="3" height="5" fill="currentColor" transform="rotate(30,88.11,72.00)"/>
|
||
<rect x="82.21" y="75.78" width="3" height="5" fill="currentColor" transform="rotate(40,83.71,78.28)"/>
|
||
<rect x="76.78" y="81.21" width="3" height="5" fill="currentColor" transform="rotate(50,78.28,83.71)"/>
|
||
<rect x="70.50" y="85.61" width="3" height="5" fill="currentColor" transform="rotate(60,72.00,88.11)"/>
|
||
<rect x="63.55" y="88.85" width="3" height="5" fill="currentColor" transform="rotate(70,65.05,91.35)"/>
|
||
<rect x="56.14" y="90.83" width="3" height="5" fill="currentColor" transform="rotate(80,57.64,93.33)"/>
|
||
<rect x="48.50" y="91.50" width="3" height="5" fill="currentColor" transform="rotate(90,50.00,94.00)"/>
|
||
<rect x="40.86" y="90.83" width="3" height="5" fill="currentColor" transform="rotate(100,42.36,93.33)"/>
|
||
<rect x="33.45" y="88.85" width="3" height="5" fill="currentColor" transform="rotate(110,34.95,91.35)"/>
|
||
<rect x="26.50" y="85.61" width="3" height="5" fill="currentColor" transform="rotate(120,28.00,88.11)"/>
|
||
<rect x="20.22" y="81.21" width="3" height="5" fill="currentColor" transform="rotate(130,21.72,83.71)"/>
|
||
<rect x="14.79" y="75.78" width="3" height="5" fill="currentColor" transform="rotate(140,16.29,78.28)"/>
|
||
<rect x="10.39" y="69.50" width="3" height="5" fill="currentColor" transform="rotate(150,11.89,72.00)"/>
|
||
<rect x="7.15" y="62.55" width="3" height="5" fill="currentColor" transform="rotate(160,8.65,65.05)"/>
|
||
<rect x="5.17" y="55.14" width="3" height="5" fill="currentColor" transform="rotate(170,6.67,57.64)"/>
|
||
<rect x="4.50" y="47.50" width="3" height="5" fill="currentColor" transform="rotate(180,6.00,50.00)"/>
|
||
<rect x="5.17" y="39.86" width="3" height="5" fill="currentColor" transform="rotate(190,6.67,42.36)"/>
|
||
<rect x="7.15" y="32.45" width="3" height="5" fill="currentColor" transform="rotate(200,8.65,34.95)"/>
|
||
<rect x="10.39" y="25.50" width="3" height="5" fill="currentColor" transform="rotate(210,11.89,28.00)"/>
|
||
<rect x="14.79" y="19.22" width="3" height="5" fill="currentColor" transform="rotate(220,16.29,21.72)"/>
|
||
<rect x="20.22" y="13.79" width="3" height="5" fill="currentColor" transform="rotate(230,21.72,16.29)"/>
|
||
<rect x="26.50" y="9.39" width="3" height="5" fill="currentColor" transform="rotate(240,28.00,11.89)"/>
|
||
<rect x="33.45" y="6.15" width="3" height="5" fill="currentColor" transform="rotate(250,34.95,8.65)"/>
|
||
<rect x="40.86" y="4.17" width="3" height="5" fill="currentColor" transform="rotate(260,42.36,6.67)"/>
|
||
<rect x="48.50" y="3.50" width="3" height="5" fill="currentColor" transform="rotate(270,50.00,6.00)"/>
|
||
<rect x="56.14" y="4.17" width="3" height="5" fill="currentColor" transform="rotate(280,57.64,6.67)"/>
|
||
<rect x="63.55" y="6.15" width="3" height="5" fill="currentColor" transform="rotate(290,65.05,8.65)"/>
|
||
<rect x="70.50" y="9.39" width="3" height="5" fill="currentColor" transform="rotate(300,72.00,11.89)"/>
|
||
<rect x="76.78" y="13.79" width="3" height="5" fill="currentColor" transform="rotate(310,78.28,16.29)"/>
|
||
<rect x="82.21" y="19.22" width="3" height="5" fill="currentColor" transform="rotate(320,83.71,21.72)"/>
|
||
<rect x="86.61" y="25.50" width="3" height="5" fill="currentColor" transform="rotate(330,88.11,28.00)"/>
|
||
<rect x="89.85" y="32.45" width="3" height="5" fill="currentColor" transform="rotate(340,91.35,34.95)"/>
|
||
<rect x="91.83" y="39.86" width="3" height="5" fill="currentColor" transform="rotate(350,93.33,42.36)"/>
|
||
<!-- Шина -->
|
||
<circle cx="50" cy="50" r="45" fill="currentColor" opacity="0.1"/>
|
||
<circle cx="50" cy="50" r="42" fill="none" stroke="currentColor" stroke-width="6"/>
|
||
<!-- Обод: двойное кольцо -->
|
||
<circle cx="50" cy="50" r="34" fill="none" stroke="currentColor" stroke-width="2"/>
|
||
<circle cx="50" cy="50" r="31" fill="none" stroke="currentColor" stroke-width="1"/>
|
||
<!-- Спицы: 32 штуки -->
|
||
<line x1="57.00" y1="50.00" x2="80.00" y2="50.00" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="56.87" y1="51.37" x2="79.42" y2="55.85" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="56.47" y1="52.68" x2="77.72" y2="61.48" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="55.82" y1="53.89" x2="74.94" y2="66.67" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="54.95" y1="54.95" x2="71.21" y2="71.21" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="53.89" y1="55.82" x2="66.67" y2="74.94" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="52.68" y1="56.47" x2="61.48" y2="77.72" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="51.37" y1="56.87" x2="55.85" y2="79.42" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="50.00" y1="57.00" x2="50.00" y2="80.00" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="48.63" y1="56.87" x2="44.15" y2="79.42" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="47.32" y1="56.47" x2="38.52" y2="77.72" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="46.11" y1="55.82" x2="33.33" y2="74.94" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="45.05" y1="54.95" x2="28.79" y2="71.21" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="44.18" y1="53.89" x2="25.06" y2="66.67" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="43.53" y1="52.68" x2="22.28" y2="61.48" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="43.13" y1="51.37" x2="20.58" y2="55.85" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="43.00" y1="50.00" x2="20.00" y2="50.00" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="43.13" y1="48.63" x2="20.58" y2="44.15" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="43.53" y1="47.32" x2="22.28" y2="38.52" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="44.18" y1="46.11" x2="25.06" y2="33.33" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="45.05" y1="45.05" x2="28.79" y2="28.79" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="46.11" y1="44.18" x2="33.33" y2="25.06" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="47.32" y1="43.53" x2="38.52" y2="22.28" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="48.63" y1="43.13" x2="44.15" y2="20.58" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="50.00" y1="43.00" x2="50.00" y2="20.00" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="51.37" y1="43.13" x2="55.85" y2="20.58" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="52.68" y1="43.53" x2="61.48" y2="22.28" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="53.89" y1="44.18" x2="66.67" y2="25.06" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="54.95" y1="45.05" x2="71.21" y2="28.79" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="55.82" y1="46.11" x2="74.94" y2="33.33" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="56.47" y1="47.32" x2="77.72" y2="38.52" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<line x1="56.87" y1="48.63" x2="79.42" y2="44.15" stroke="currentColor" stroke-width="0.8" opacity="0.7"/>
|
||
<!-- Ступица -->
|
||
<circle cx="50" cy="50" r="8" fill="currentColor" opacity="0.3"/>
|
||
<circle cx="50" cy="50" r="8" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||
<!-- Болты ступицы: 8 штук -->
|
||
<circle cx="62.00" cy="50.00" r="1.5" fill="currentColor"/>
|
||
<circle cx="58.49" cy="58.49" r="1.5" fill="currentColor"/>
|
||
<circle cx="50.00" cy="62.00" r="1.5" fill="currentColor"/>
|
||
<circle cx="41.51" cy="58.49" r="1.5" fill="currentColor"/>
|
||
<circle cx="38.00" cy="50.00" r="1.5" fill="currentColor"/>
|
||
<circle cx="41.51" cy="41.51" r="1.5" fill="currentColor"/>
|
||
<circle cx="50.00" cy="38.00" r="1.5" fill="currentColor"/>
|
||
<circle cx="58.49" cy="41.51" r="1.5" fill="currentColor"/>
|
||
<!-- Центр -->
|
||
<circle cx="50" cy="50" r="3" fill="currentColor"/>
|
||
</svg>
|
||
<div class="mini-route-dot" id="mini-dot"></div>
|
||
<div class="mini-route-text">
|
||
<div class="mini-route-label" id="mini-label">Вариант 1</div>
|
||
<div class="mini-route-stats" id="mini-stats">— км · —% грунт</div>
|
||
</div>
|
||
<div class="mini-route-arrows">
|
||
<span class="mini-arrow" id="mini-prev">‹</span>
|
||
<span class="mini-arrow" id="mini-next">›</span>
|
||
</div>
|
||
<button class="mini-add-btn" id="mini-add-btn" onclick="miniAddWaypoint()" title="Добавить точку">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Scripts -->
|
||
<script src="https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.js"></script>
|
||
<script src="https://unpkg.com/suncalc@1.9.0/suncalc.min.js"></script>
|
||
<!-- ET-005: units.js ДОЛЖЕН подключаться строго перед app.js (ADR-0001 п.2, риск R7) -->
|
||
<script src="units.js"></script>
|
||
<script src="app.js"></script>
|
||
<!-- ET-006: gpx.js подключается после app.js — потребляет его глобали (ADR-002) -->
|
||
<script src="gpx.js"></script>
|
||
</body>
|
||
</html>
|