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 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,8 +123,79 @@
|
|||||||
|
|
||||||
// ── 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;
|
||||||
|
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;
|
activePlanId = null;
|
||||||
addWaypoint(e.lngLat.lng, e.lngLat.lat);
|
fetchRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWaypoint(lng, lat) {
|
function addWaypoint(lng, lat) {
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user