Route insertion: larger snap marker, wider hit-test, thicker route line
This commit is contained in:
+95
-2
@@ -24,6 +24,10 @@
|
||||
let saveName = $state('');
|
||||
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 = [
|
||||
{ id: 'fastbike', label: 'Road' },
|
||||
{ id: 'gravel', label: 'Gravel' },
|
||||
@@ -102,11 +106,16 @@
|
||||
id: 'route-line',
|
||||
type: 'line',
|
||||
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('mousemove', onMapMouseMove);
|
||||
loadPlans();
|
||||
});
|
||||
|
||||
@@ -114,8 +123,79 @@
|
||||
|
||||
// ── Waypoint management ────────────────────────────────────────────────────
|
||||
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;
|
||||
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;
|
||||
addWaypoint(e.lngLat.lng, e.lngLat.lat);
|
||||
fetchRoute();
|
||||
}
|
||||
|
||||
function addWaypoint(lng, lat) {
|
||||
@@ -162,6 +242,9 @@
|
||||
waypoints = [];
|
||||
route = null;
|
||||
error = '';
|
||||
hoveringRoute = false;
|
||||
snapMarker?.remove();
|
||||
map.getCanvas().style.cursor = '';
|
||||
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-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) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
Reference in New Issue
Block a user