Route insertion: larger snap marker, wider hit-test, thicker route line

This commit is contained in:
Davide Scaini
2026-05-14 10:10:57 +02:00
parent ed90a1830b
commit 89f76829b7
+94 -1
View File
@@ -24,6 +24,10 @@
let saveName = $state(''); let saveName = $state('');
let saving = $state(false); let saving = $state(false);
// Route insertion (hover snap)
let hoveringRoute = false; // plain JS — no need to render
let snapMarker; // maplibre Marker used as snap indicator
const PROFILES = [ const PROFILES = [
{ id: 'fastbike', label: 'Road' }, { id: 'fastbike', label: 'Road' },
{ id: 'gravel', label: 'Gravel' }, { id: 'gravel', label: 'Gravel' },
@@ -102,11 +106,16 @@
id: 'route-line', id: 'route-line',
type: 'line', type: 'line',
source: 'route', source: 'route',
paint: { 'line-color': '#e879a0', 'line-width': 3, 'line-opacity': 0.9 }, paint: { 'line-color': '#e879a0', 'line-width': 4, 'line-opacity': 0.9 },
}); });
}); });
const snapEl = document.createElement('div');
snapEl.className = 'snap-marker';
snapMarker = new maplibregl.Marker({ element: snapEl, draggable: false });
map.on('click', onMapClick); map.on('click', onMapClick);
map.on('mousemove', onMapMouseMove);
loadPlans(); loadPlans();
}); });
@@ -114,9 +123,80 @@
// ── Waypoint management ──────────────────────────────────────────────────── // ── Waypoint management ────────────────────────────────────────────────────
function onMapClick(e) { function onMapClick(e) {
if (hoveringRoute && route && waypoints.length >= 2) {
const snapped = nearestOnLine(route.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat]);
const idx = insertionIndex(snapped.frac, route.geometry.coordinates);
insertWaypointAt(snapped.lng, snapped.lat, idx);
} else {
activePlanId = null; activePlanId = null;
addWaypoint(e.lngLat.lng, e.lngLat.lat); addWaypoint(e.lngLat.lng, e.lngLat.lat);
} }
}
function onMapMouseMove(e) {
if (!route || waypoints.length < 2) {
if (hoveringRoute) { hoveringRoute = false; snapMarker.remove(); map.getCanvas().style.cursor = ''; }
return;
}
const b = 14;
const features = map.queryRenderedFeatures(
[[e.point.x - b, e.point.y - b], [e.point.x + b, e.point.y + b]],
{ layers: ['route-line'] }
);
if (features.length > 0) {
hoveringRoute = true;
map.getCanvas().style.cursor = 'crosshair';
const snapped = nearestOnLine(route.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat]);
snapMarker.setLngLat([snapped.lng, snapped.lat]).addTo(map);
} else {
hoveringRoute = false;
snapMarker.remove();
map.getCanvas().style.cursor = '';
}
}
// ── Route insertion helpers ────────────────────────────────────────────────
function nearestOnLine(coords, [px, py]) {
let bestDist = Infinity, bestLng = px, bestLat = py, bestFrac = 0;
for (let i = 0; i < coords.length - 1; i++) {
const [x1, y1] = coords[i];
const [x2, y2] = coords[i + 1];
const dx = x2 - x1, dy = y2 - y1;
const lenSq = dx * dx + dy * dy;
const t = lenSq > 0 ? Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq)) : 0;
const nx = x1 + t * dx, ny = y1 + t * dy;
const d = Math.hypot(px - nx, py - ny);
if (d < bestDist) { bestDist = d; bestLng = nx; bestLat = ny; bestFrac = i + t; }
}
return { lng: bestLng, lat: bestLat, frac: bestFrac };
}
function insertionIndex(clickFrac, coords) {
const fracs = waypoints.map(wp => nearestOnLine(coords, [wp.lng, wp.lat]).frac);
for (let i = 0; i < fracs.length - 1; i++) {
if (clickFrac >= fracs[i] && clickFrac <= fracs[i + 1]) return i + 1;
}
return waypoints.length;
}
function insertWaypointAt(lng, lat, idx) {
const el = document.createElement('div');
el.className = 'wp-marker';
const marker = new maplibregl.Marker({ element: el, draggable: true })
.setLngLat([lng, lat])
.addTo(map);
marker.on('dragend', () => {
const { lng: newLng, lat: newLat } = marker.getLngLat();
const i = waypoints.findIndex(w => w.marker === marker);
if (i >= 0) { waypoints[i].lng = newLng; waypoints[i].lat = newLat; }
activePlanId = null;
fetchRoute();
});
waypoints = [...waypoints.slice(0, idx), { lng, lat, marker }, ...waypoints.slice(idx)];
waypoints.forEach((w, i) => { w.marker.getElement().textContent = i + 1; });
activePlanId = null;
fetchRoute();
}
function addWaypoint(lng, lat) { function addWaypoint(lng, lat) {
const el = document.createElement('div'); const el = document.createElement('div');
@@ -162,6 +242,9 @@
waypoints = []; waypoints = [];
route = null; route = null;
error = ''; error = '';
hoveringRoute = false;
snapMarker?.remove();
map.getCanvas().style.cursor = '';
if (map.getSource('route')) map.getSource('route').setData(emptyGeoJSON()); if (map.getSource('route')) map.getSource('route').setData(emptyGeoJSON());
} }
@@ -583,6 +666,16 @@ ${trkpts}
.plan-name { font-size: 0.8rem; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .plan-name { font-size: 0.8rem; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.plan-meta { font-size: 0.7rem; color: var(--text-5); } .plan-meta { font-size: 0.7rem; color: var(--text-5); }
:global(.snap-marker) {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2.5px solid var(--accent);
background: rgba(232, 121, 160, 0.25);
box-shadow: 0 0 0 2px rgba(232, 121, 160, 0.15);
pointer-events: none;
}
:global(.wp-marker) { :global(.wp-marker) {
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;