auto-sync: 2026-05-05 19:30:01
This commit is contained in:
@@ -284,25 +284,26 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
.wl-remove:active { background: var(--red-bg); color: var(--red); }
|
||||
.wl-remove svg { width: 14px; height: 14px; }
|
||||
|
||||
/* Route actions */
|
||||
.route-actions {
|
||||
display: flex; gap: 8px; margin: 8px 0;
|
||||
}
|
||||
.btn-action {
|
||||
height: 36px; padding: 0 14px;
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||
background: var(--surface2); border: 1px solid var(--border);
|
||||
border-radius: 12px; color: var(--text2);
|
||||
font-size: 13px; font-weight: 600; cursor: pointer;
|
||||
/* Sheet icon buttons (header) */
|
||||
.sheet-icon-btn {
|
||||
width: 32px; height: 32px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: none; border: none; color: var(--text3);
|
||||
border-radius: 8px; cursor: pointer; padding: 0;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: background 0.15s, transform 0.1s, color 0.15s;
|
||||
}
|
||||
.btn-action svg { width: 16px; height: 16px; flex-shrink: 0; transition: transform 0.1s; }
|
||||
.btn-action:active { transform: scale(0.94); background: var(--surface3, var(--border)); }
|
||||
.btn-action.primary { border-color: var(--accent); color: var(--accent); }
|
||||
.btn-action.primary:active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.btn-action.danger { color: var(--red); }
|
||||
.btn-action.danger:active { background: var(--red); color: #fff; border-color: var(--red); }
|
||||
.sheet-icon-btn svg { width: 18px; height: 18px; }
|
||||
.sheet-icon-btn:active { background: var(--surface2); }
|
||||
.sheet-icon-btn.danger { color: var(--red); }
|
||||
.sheet-icon-btn.danger:active { background: var(--red); color: #fff; }
|
||||
|
||||
/* Add waypoint row */
|
||||
.wl-add { cursor: pointer; }
|
||||
.wl-add:active { background: var(--surface); }
|
||||
.wl-add .wl-pin svg path { fill: var(--text3) !important; }
|
||||
.wl-add .wl-label { color: var(--text3); }
|
||||
|
||||
/* ── Route Status ─────────────────────────────── */
|
||||
#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
72
tasks/enduro-trails/prototype/static/app.js
vendored
72
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -443,7 +443,6 @@ function clearRoute() {
|
||||
}
|
||||
}
|
||||
document.getElementById('route-status').textContent = 'Тапни точку старта на карте';
|
||||
document.getElementById('route-actions').style.display = 'none';
|
||||
document.getElementById('route-cards').innerHTML = '';
|
||||
document.getElementById('waypoints-list').innerHTML = '';
|
||||
if (routeMode && map) map.getCanvas().style.cursor = 'crosshair';
|
||||
@@ -533,7 +532,7 @@ async function renderWaypointsList() {
|
||||
const list = document.getElementById('waypoints-list');
|
||||
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
|
||||
|
||||
list.innerHTML = routeWaypoints.map((wp, i) => {
|
||||
let html = routeWaypoints.map((wp, i) => {
|
||||
const isStart = i === 0;
|
||||
const isEnd = i === routeWaypoints.length - 1;
|
||||
const label = isStart ? 'S' : isEnd ? 'F' : String(i);
|
||||
@@ -548,6 +547,16 @@ async function renderWaypointsList() {
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Кнопка «Добавить точку» в стиле wl-item
|
||||
if (routeWaypoints.length < 10) {
|
||||
html += `<div class="wl-item wl-add" onclick="addWaypointMode()">
|
||||
<div class="wl-pin">${waypointPinSvg('+', 'var(--text3)')}</div>
|
||||
<span class="wl-label">Добавить точку</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
list.innerHTML = html;
|
||||
|
||||
// Async geocode
|
||||
routeWaypoints.forEach(async (wp, i) => {
|
||||
const name = await reverseGeocode(wp.lat, wp.lon);
|
||||
@@ -571,8 +580,6 @@ function removeWaypoint(idx) {
|
||||
}
|
||||
routeResults = [];
|
||||
document.getElementById('route-cards').innerHTML = '';
|
||||
document.getElementById('route-actions').style.display =
|
||||
routeWaypoints.length >= 2 ? 'block' : 'none';
|
||||
document.getElementById('route-status').textContent =
|
||||
routeWaypoints.length === 0 ? 'Тапни точку старта на карте' :
|
||||
routeWaypoints.length === 1 ? 'Тапни точку финиша' : '';
|
||||
@@ -607,7 +614,6 @@ async function buildRoute() {
|
||||
drawRouteResults(routeResults, 0);
|
||||
|
||||
document.getElementById('route-status').textContent = `${routeResults.length} маршрут(ов)`;
|
||||
document.getElementById('route-actions').style.display = 'block';
|
||||
} catch(e) {
|
||||
document.getElementById('route-status').textContent = '❌ ' + e.message;
|
||||
document.getElementById('route-cards').innerHTML = '';
|
||||
@@ -737,16 +743,14 @@ function escapeXml(str) {
|
||||
return (str || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function downloadGPX() {
|
||||
function generateGPX() {
|
||||
const route = routeResults[activeRouteIdx];
|
||||
if (!route) return;
|
||||
if (!route) return '';
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString().slice(0, 10);
|
||||
const timeStr = now.toISOString().replace(/[-:]/g, '').slice(0, 15);
|
||||
const filename = `enduro-${timeStr}.gpx`;
|
||||
const distKm = (route.distance_m / 1000).toFixed(1);
|
||||
const dirtPct = route.stats ? route.stats.dirt_total_pct : '?';
|
||||
|
||||
|
||||
const wpts = routeWaypoints.map((wp, i) => {
|
||||
const name = i === 0 ? 'Старт' : i === routeWaypoints.length - 1 ? 'Финиш' : `Точка ${i}`;
|
||||
return ` <wpt lat="${wp.lat}" lon="${wp.lon}"><name>${escapeXml(name)}</name></wpt>`;
|
||||
@@ -755,12 +759,12 @@ function downloadGPX() {
|
||||
markers.forEach(m => {
|
||||
wpts.push(` <wpt lat="${m.lat}" lon="${m.lon}"><name>${escapeXml(m.name)}</name><sym>${escapeXml(m.icon)}</sym></wpt>`);
|
||||
});
|
||||
|
||||
|
||||
const trkpts = route.geometry.coordinates.map(([lon, lat]) =>
|
||||
` <trkpt lat="${lat}" lon="${lon}"/>`
|
||||
).join('\n');
|
||||
|
||||
const gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="Enduro Trails" xmlns="http://www.topografix.com/GPX/1/1">
|
||||
<metadata>
|
||||
<name>Enduro route ${dateStr}</name>
|
||||
@@ -775,7 +779,14 @@ ${trkpts}
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>`;
|
||||
|
||||
}
|
||||
|
||||
function downloadGPX() {
|
||||
const gpx = generateGPX();
|
||||
if (!gpx) return;
|
||||
const now = new Date();
|
||||
const timeStr = now.toISOString().replace(/[-:]/g, '').slice(0, 15);
|
||||
const filename = `enduro-${timeStr}.gpx`;
|
||||
const blob = new Blob([gpx], { type: 'application/gpx+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -785,6 +796,38 @@ ${trkpts}
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function shareRoute() {
|
||||
const route = routeResults[activeRouteIdx];
|
||||
if (!route) { alert('Сначала построй маршрут'); return; }
|
||||
|
||||
const distKm = (route.distance_m / 1000).toFixed(0);
|
||||
const dirtPct = route.stats ? route.stats.dirt_total_pct : 0;
|
||||
const shareText = `Маршрут: ${distKm} км, ${dirtPct}% грунт`;
|
||||
const gpx = generateGPX();
|
||||
const blob = new Blob([gpx], { type: 'application/gpx+xml' });
|
||||
const file = new File([blob], 'enduro-route.gpx', { type: 'application/gpx+xml' });
|
||||
|
||||
// 1. File share (HTTPS only)
|
||||
if (navigator.share && location.protocol === 'https:') {
|
||||
try {
|
||||
const canShare = navigator.canShare && navigator.canShare({ files: [file] });
|
||||
if (canShare) {
|
||||
await navigator.share({ title: 'Enduro Route', text: shareText, files: [file] });
|
||||
return;
|
||||
}
|
||||
// canShare rejected files → share text only
|
||||
await navigator.share({ title: 'Enduro Route', text: shareText });
|
||||
return;
|
||||
} catch(e) {
|
||||
if (e.name === 'AbortError') return;
|
||||
console.warn('Share failed, falling back to download:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. HTTPS unavailable → download GPX directly
|
||||
downloadGPX();
|
||||
}
|
||||
|
||||
// ─── Флажки / именованные метки ────────────────────────────────────
|
||||
const MARKER_ICONS = ['🚩', '⛺', '🔧', '⛽', '💧', '📍'];
|
||||
const MARKERS_KEY = 'enduro_markers';
|
||||
@@ -1089,7 +1132,6 @@ function initRouteClicks(map) {
|
||||
} else if (routeWaypoints.length === 1) {
|
||||
routeWaypoints.push({ lon: lng, lat: lat });
|
||||
rebuildWaypointMarkers(); renderWaypointsList();
|
||||
document.getElementById('route-actions').style.display = 'block';
|
||||
buildRoute();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user