679 lines
31 KiB
JavaScript
679 lines
31 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
var avoidHighwaysCheckbox = document.getElementById('avoidHighways');
|
|
var useShortestCheckbox = document.getElementById('useShortest');
|
|
var avoidTollRoadsCheckbox = document.getElementById('avoidTollRoads');
|
|
var avoidFerriesCheckbox = document.getElementById('avoidFerries');
|
|
var avoidUnpavedCheckbox = document.getElementById('avoidUnpaved');
|
|
var points = [];
|
|
var markers = [];
|
|
var routePolyline = null;
|
|
var snapLines = [];
|
|
var originalPoints = [];
|
|
var lastRouteLatLngs = null; // concatenated
|
|
var lastRtePoints = []; // for GPX <rtept>
|
|
var waypointListEl = document.getElementById('waypointList');
|
|
var nextBanner = document.getElementById('nextManeuverBanner');
|
|
var voiceToggle = document.getElementById('voiceToggle');
|
|
var exportModeSel = document.getElementById('exportMode');
|
|
var roundTripToggle = document.getElementById('roundTripToggle');
|
|
var roundTripKmInput = document.getElementById('roundTripKm');
|
|
var roundTripBtn = document.getElementById('roundTripBtn');
|
|
|
|
function renderWaypointList() {
|
|
if (!waypointListEl) return;
|
|
waypointListEl.innerHTML = '';
|
|
points.forEach(function(p, idx){
|
|
var li = document.createElement('li');
|
|
li.className = 'list-group-item d-flex justify-content-between align-items-center';
|
|
var label = (idx === 0) ? 'Start' : (idx === points.length - 1 ? 'End' : `WP ${idx}`);
|
|
var coords = `${p.lat.toFixed(5)}, ${p.lng.toFixed(5)}`;
|
|
li.innerHTML = `<span>${label}<br><small class="text-secondary">${coords}</small></span>`;
|
|
var btns = document.createElement('div');
|
|
btns.className = 'btn-group btn-group-sm';
|
|
var up = document.createElement('button'); up.className = 'btn btn-outline-secondary'; up.textContent = '↑'; up.disabled = (idx === 0);
|
|
var down = document.createElement('button'); down.className = 'btn btn-outline-secondary'; down.textContent = '↓'; down.disabled = (idx === points.length - 1);
|
|
var del = document.createElement('button'); del.className = 'btn btn-outline-danger'; del.textContent = '✕';
|
|
up.onclick = function(){ if (idx > 0) { swapWaypoints(idx, idx-1); } };
|
|
down.onclick = function(){ if (idx < points.length - 1) { swapWaypoints(idx, idx+1); } };
|
|
del.onclick = function(){ removeWaypoint(idx); };
|
|
btns.appendChild(up); btns.appendChild(down); btns.appendChild(del);
|
|
li.appendChild(btns);
|
|
waypointListEl.appendChild(li);
|
|
});
|
|
}
|
|
|
|
function swapWaypoints(i, j) {
|
|
var tmp = points[i]; points[i] = points[j]; points[j] = tmp;
|
|
var tmpo = originalPoints[i]; originalPoints[i] = originalPoints[j]; originalPoints[j] = tmpo;
|
|
var tmpm = markers[i]; markers[i] = markers[j]; markers[j] = tmpm;
|
|
renderWaypointList();
|
|
calculateRoute();
|
|
}
|
|
|
|
function removeWaypoint(i) {
|
|
if (markers[i]) { map.removeLayer(markers[i]); }
|
|
markers.splice(i,1);
|
|
points.splice(i,1);
|
|
originalPoints.splice(i,1);
|
|
renderWaypointList();
|
|
calculateRoute();
|
|
}
|
|
|
|
// Geo helpers for round trip generation
|
|
function toRad(d){ return d * Math.PI / 180; }
|
|
function toDeg(r){ return r * 180 / Math.PI; }
|
|
function destPoint(lat, lon, distanceMeters, bearingDeg) {
|
|
var R = 6371000; // meters
|
|
var br = toRad(bearingDeg);
|
|
var φ1 = toRad(lat), λ1 = toRad(lon);
|
|
var δ = distanceMeters / R;
|
|
var sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1);
|
|
var sinδ = Math.sin(δ), cosδ = Math.cos(δ);
|
|
var sinφ2 = sinφ1 * cosδ + cosφ1 * sinδ * Math.cos(br);
|
|
var φ2 = Math.asin(sinφ2);
|
|
var y = Math.sin(br) * sinδ * cosφ1;
|
|
var x = cosδ - sinφ1 * sinφ2;
|
|
var λ2 = λ1 + Math.atan2(y, x);
|
|
return { lat: toDeg(φ2), lng: ((toDeg(λ2) + 540) % 360) - 180 };
|
|
}
|
|
|
|
function generateRoundTripWaypoints(start) {
|
|
var km = parseFloat(roundTripKmInput && roundTripKmInput.value ? roundTripKmInput.value : '100');
|
|
if (isNaN(km) || km < 5) km = 50;
|
|
// Approximate radius for a loop with 3 intermediate waypoints: perimeter ≈ 4 * chord ≈ distance
|
|
// Use radius r ≈ distance / (2π)
|
|
var meters = km * 1000;
|
|
var r = Math.max(500, meters / (2 * Math.PI));
|
|
// Bearings spaced around circle with a random offset to vary loops
|
|
var base = (Math.random() * 360) | 0;
|
|
var noRepeat = localStorage.getItem('freemoto-roundtrip-norepeat') === '1';
|
|
var bearings = noRepeat
|
|
? [0, 72, 144, 216, 288].map(function(d){ return base + d; }) // 5 WPs ~ evenly spaced to reduce overlap
|
|
: [45, 165, 285].map(function(d){ return base + d; });
|
|
var wps = bearings.map(function(b){ return destPoint(start.lat, start.lng, r, b); });
|
|
// Ensure endpoints list: start -> wps -> start
|
|
var routePts = [start].concat(wps).concat([start]);
|
|
return routePts;
|
|
}
|
|
|
|
function createRoundTrip() {
|
|
if (!roundTripToggle || !roundTripToggle.checked) return;
|
|
// Determine start point: prefer first existing point, else sourceInput coords
|
|
var start = null;
|
|
if (points.length > 0) {
|
|
start = { lat: points[0].lat, lng: points[0].lng };
|
|
} else {
|
|
var sourceInput = document.getElementById('sourceInput');
|
|
var sLat = parseFloat(sourceInput && sourceInput.dataset.lat);
|
|
var sLon = parseFloat(sourceInput && sourceInput.dataset.lon);
|
|
if (!isNaN(sLat) && !isNaN(sLon)) {
|
|
start = { lat: sLat, lng: sLon };
|
|
} else {
|
|
// fallback: map center
|
|
var c = map.getCenter();
|
|
start = { lat: c.lat, lng: c.lng };
|
|
}
|
|
}
|
|
|
|
// Clear existing markers/lines
|
|
markers.forEach(function(m){ map.removeLayer(m); });
|
|
markers = [];
|
|
points = [];
|
|
originalPoints = [];
|
|
if (routePolyline) { map.removeLayer(routePolyline); routePolyline = null; }
|
|
if (snapLines && snapLines.length) { snapLines.forEach(function(sl){ map.removeLayer(sl); }); snapLines = []; }
|
|
|
|
var pts = generateRoundTripWaypoints(start);
|
|
pts.forEach(function(p, idx){
|
|
var marker = L.marker(p, { draggable: true }).addTo(map);
|
|
marker.bindPopup(idx === 0 ? 'Start/End' : `WP ${idx}`).openPopup();
|
|
marker.on('dragend', function(ev){
|
|
var m = ev.target; var newLL = m.getLatLng(); var i = markers.indexOf(m);
|
|
if (i >= 0) { points[i] = { lat: newLL.lat, lng: newLL.lng }; originalPoints[i] = { lat: newLL.lat, lng: newLL.lng }; renderWaypointList(); calculateRoute(); }
|
|
});
|
|
markers.push(marker);
|
|
points.push({ lat: p.lat, lng: p.lng });
|
|
originalPoints.push({ lat: p.lat, lng: p.lng });
|
|
});
|
|
renderWaypointList();
|
|
calculateRoute();
|
|
}
|
|
|
|
function calculateRoute() {
|
|
if (points.length >= 2) {
|
|
var moto = {};
|
|
// Avoid highways -> lower highway usage weight
|
|
if (avoidHighwaysCheckbox && avoidHighwaysCheckbox.checked) {
|
|
moto.use_highways = 0.0; // 0..1 (0 avoids, 1 prefers)
|
|
}
|
|
// Avoid ferries -> lower ferry usage weight
|
|
if (avoidFerriesCheckbox && avoidFerriesCheckbox.checked) {
|
|
moto.use_ferry = 0.0; // 0..1
|
|
}
|
|
// Avoid unpaved -> exclude unpaved roads entirely
|
|
if (avoidUnpavedCheckbox && avoidUnpavedCheckbox.checked) {
|
|
moto.exclude_unpaved = true;
|
|
}
|
|
// Avoid tolls -> exclude tolls
|
|
if (avoidTollRoadsCheckbox && avoidTollRoadsCheckbox.checked) {
|
|
moto.exclude_tolls = true;
|
|
}
|
|
|
|
// Twistiness slider from localStorage: 0 (straight/fast) .. 100 (very twisty)
|
|
try {
|
|
var twistVal = parseInt(localStorage.getItem('freemoto-twistiness') || '50', 10);
|
|
if (!isNaN(twistVal)) {
|
|
// Map 0..100 to highway preference 1.0..0.0 (inverse)
|
|
var hw = Math.max(0, Math.min(1, 1 - (twistVal / 100)));
|
|
// If user also checked avoidHighways, keep the stronger effect (min)
|
|
moto.use_highways = (typeof moto.use_highways === 'number') ? Math.min(moto.use_highways, hw) : hw;
|
|
}
|
|
} catch (_) {}
|
|
|
|
// Highway preference slider directly sets use_highways proportionally
|
|
try {
|
|
var hwp = parseInt(localStorage.getItem('freemoto-highways') || '50', 10);
|
|
if (!isNaN(hwp)) {
|
|
var hw2 = Math.max(0, Math.min(1, hwp / 100));
|
|
// Combine with existing (take max so user can prefer highways even if twistiness low)
|
|
moto.use_highways = (typeof moto.use_highways === 'number') ? Math.max(moto.use_highways, hw2) : hw2;
|
|
}
|
|
} catch (_) {}
|
|
|
|
var costing_options = { motorcycle: moto };
|
|
|
|
var requestBody = {
|
|
locations: points.map(function(p){ return { lat: p.lat, lon: p.lng }; }),
|
|
costing: "motorcycle",
|
|
costing_options: costing_options,
|
|
units: "kilometers"
|
|
};
|
|
if (useShortestCheckbox && useShortestCheckbox.checked) {
|
|
requestBody.shortest = true; // top-level shortest flag
|
|
}
|
|
|
|
fetch('/route', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(requestBody)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
var legs = (data.trip && Array.isArray(data.trip.legs)) ? data.trip.legs : [];
|
|
var infoCard = document.getElementById('routeInfoCard');
|
|
// Sum summaries across legs if needed
|
|
var totalLen = 0, totalTime = 0;
|
|
legs.forEach(function(leg){
|
|
if (leg && leg.summary) {
|
|
if (typeof leg.summary.length === 'number') totalLen += leg.summary.length;
|
|
if (typeof leg.summary.time === 'number') totalTime += leg.summary.time;
|
|
}
|
|
});
|
|
if (legs.length > 0) {
|
|
var distanceKm = (totalLen).toFixed(1);
|
|
var durationMin = Math.round(totalTime / 60);
|
|
var info = `<strong>Distance:</strong> ${distanceKm} km<br>
|
|
<strong>Estimated Time:</strong> ${durationMin} min<br>
|
|
<strong>Motorcycle Profile</strong>`;
|
|
infoCard.innerHTML = info;
|
|
infoCard.classList.remove('d-none');
|
|
|
|
// Update summary pill in app bar
|
|
var pill = document.getElementById('summaryPill');
|
|
if (pill) {
|
|
pill.textContent = `${distanceKm} km · ${durationMin} min`;
|
|
pill.classList.remove('d-none');
|
|
}
|
|
} else {
|
|
infoCard.innerHTML = `<strong>Route info unavailable.</strong>`;
|
|
infoCard.classList.remove('d-none');
|
|
console.log('Valhalla response:', data);
|
|
}
|
|
// Draw route across all legs
|
|
var latlngs = [];
|
|
var legsLatLngs = [];
|
|
legs.forEach(function(leg){
|
|
var lls = decodePolyline6(leg.shape || '');
|
|
legsLatLngs.push(lls);
|
|
if (latlngs.length && lls.length) {
|
|
// avoid duplicating the joint point
|
|
latlngs = latlngs.concat(lls.slice(1));
|
|
} else {
|
|
latlngs = latlngs.concat(lls);
|
|
}
|
|
});
|
|
if (routePolyline) {
|
|
map.removeLayer(routePolyline);
|
|
}
|
|
routePolyline = L.polyline(latlngs, { color: 'red', weight: 5}).addTo(map);
|
|
map.fitBounds(routePolyline.getBounds());
|
|
|
|
// Build directions bottom sheet and next maneuver banner/voice
|
|
try {
|
|
var directionsSheet = document.getElementById('directionsSheet');
|
|
var directionsList = document.getElementById('directionsList');
|
|
directionsList.innerHTML = '';
|
|
lastRouteLatLngs = latlngs;
|
|
lastRtePoints = [];
|
|
// Aggregate maneuvers from all legs and render
|
|
legs.forEach(function(leg, li){
|
|
var maneuvers = leg.maneuvers || [];
|
|
var lls = legsLatLngs[li] || [];
|
|
maneuvers.forEach(function(m) {
|
|
var liEl = document.createElement('li');
|
|
liEl.className = 'list-group-item d-flex justify-content-between align-items-center';
|
|
var text = m.instruction || m.street_names?.join(', ') || 'Continue';
|
|
var meta = [];
|
|
if (typeof m.length === 'number') meta.push((m.length).toFixed(1) + ' km');
|
|
if (typeof m.time === 'number') meta.push(Math.round(m.time / 60) + ' min');
|
|
liEl.innerHTML = `<span>${text}</span><small class="text-secondary ms-2">${meta.join(' · ')}</small>`;
|
|
directionsList.appendChild(liEl);
|
|
// Store rte point for GPX
|
|
var idx = (typeof m.begin_shape_index === 'number') ? m.begin_shape_index : 0;
|
|
idx = Math.max(0, Math.min(idx, lls.length - 1));
|
|
var coord = lls[idx] || lls[0];
|
|
if (coord) {
|
|
lastRtePoints.push({ lat: coord[0], lon: coord[1], name: text });
|
|
}
|
|
});
|
|
});
|
|
|
|
// Next maneuver banner (first item)
|
|
if (directionsList.children.length > 0) {
|
|
var first = directionsList.children[0].querySelector('span');
|
|
if (first && nextBanner) {
|
|
nextBanner.textContent = first.textContent;
|
|
nextBanner.classList.remove('d-none');
|
|
}
|
|
// Voice prompt
|
|
if (voiceToggle && voiceToggle.checked && 'speechSynthesis' in window) {
|
|
try {
|
|
var utter = new SpeechSynthesisUtterance(first.textContent);
|
|
window.speechSynthesis.cancel();
|
|
window.speechSynthesis.speak(utter);
|
|
} catch (_) {}
|
|
}
|
|
} else if (nextBanner) {
|
|
nextBanner.classList.add('d-none');
|
|
}
|
|
|
|
if (directionsList.children.length > 0) {
|
|
directionsSheet.style.display = 'block';
|
|
// default to collapsed to avoid covering full map
|
|
directionsSheet.classList.add('collapsed');
|
|
// Toggle collapse on handle click
|
|
var handle = directionsSheet.querySelector('.handle');
|
|
if (handle) {
|
|
handle.onclick = function() {
|
|
directionsSheet.classList.toggle('collapsed');
|
|
};
|
|
|
|
// Drag-to-resize
|
|
var sheetCard = directionsSheet.querySelector('.sheet-card');
|
|
var sheetBody = directionsSheet.querySelector('.sheet-body');
|
|
var dragging = false;
|
|
var startY = 0;
|
|
var startHeight = 0;
|
|
var vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
|
|
|
function px(val) { return `${val}px`; }
|
|
function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); }
|
|
|
|
function onMove(clientY) {
|
|
var dy = startY - clientY; // drag up to expand
|
|
var newHeight = clamp(startHeight + dy, 0.28 * vh, 0.8 * vh);
|
|
sheetCard.style.maxHeight = px(newHeight);
|
|
if (sheetBody) sheetBody.style.maxHeight = px(newHeight - 42);
|
|
directionsSheet.classList.remove('collapsed');
|
|
}
|
|
|
|
handle.addEventListener('mousedown', function(ev) {
|
|
dragging = true;
|
|
startY = ev.clientY;
|
|
startHeight = sheetCard.getBoundingClientRect().height;
|
|
document.body.style.userSelect = 'none';
|
|
});
|
|
window.addEventListener('mousemove', function(ev) {
|
|
if (!dragging) return;
|
|
onMove(ev.clientY);
|
|
});
|
|
window.addEventListener('mouseup', function() {
|
|
if (dragging) {
|
|
dragging = false;
|
|
document.body.style.userSelect = '';
|
|
}
|
|
});
|
|
// Touch
|
|
handle.addEventListener('touchstart', function(ev) {
|
|
dragging = true;
|
|
startY = ev.touches[0].clientY;
|
|
startHeight = sheetCard.getBoundingClientRect().height;
|
|
}, {passive: true});
|
|
window.addEventListener('touchmove', function(ev) {
|
|
if (!dragging) return;
|
|
onMove(ev.touches[0].clientY);
|
|
}, {passive: true});
|
|
window.addEventListener('touchend', function() { dragging = false; });
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to render directions sheet', e);
|
|
}
|
|
|
|
// Snap-to-roads visualization for all waypoints: snapped points are
|
|
// first of leg0, then last point of each leg i (for waypoint i)
|
|
try {
|
|
// Clear previous snap lines
|
|
if (snapLines && snapLines.length) {
|
|
snapLines.forEach(function(sl){ map.removeLayer(sl); });
|
|
snapLines = [];
|
|
}
|
|
var snapped = [];
|
|
if (legsLatLngs.length > 0) {
|
|
snapped[0] = legsLatLngs[0][0];
|
|
for (var i = 1; i < legsLatLngs.length; i++) {
|
|
var lseg = legsLatLngs[i];
|
|
if (lseg && lseg.length) {
|
|
snapped[i] = lseg[lseg.length - 1];
|
|
}
|
|
}
|
|
}
|
|
for (var j = 0; j < markers.length && j < snapped.length; j++) {
|
|
if (!snapped[j]) continue;
|
|
if (originalPoints[j]) {
|
|
var sl = L.polyline([originalPoints[j], snapped[j]], { color: '#0d6efd', weight: 2, dashArray: '6,6', opacity: 0.7 });
|
|
sl.addTo(map);
|
|
snapLines.push(sl);
|
|
}
|
|
markers[j].setLatLng(snapped[j]);
|
|
}
|
|
} catch (e) { console.warn('Snap-to-roads viz failed', e); }
|
|
});
|
|
}
|
|
}
|
|
|
|
// Visual pulse effect at a latlng
|
|
function pulseAt(latlng) {
|
|
try {
|
|
var pulse = L.circle(latlng, { radius: 5, color: '#0d6efd', weight: 2, opacity: 0.8, fillOpacity: 0.2 });
|
|
pulse.addTo(map);
|
|
var steps = 10;
|
|
var i = 0;
|
|
var interval = setInterval(function() {
|
|
i++;
|
|
pulse.setRadius(5 + i * 20);
|
|
pulse.setStyle({ opacity: Math.max(0, 0.8 - i * 0.08), fillOpacity: Math.max(0, 0.2 - i * 0.02) });
|
|
if (i >= steps) {
|
|
clearInterval(interval);
|
|
map.removeLayer(pulse);
|
|
}
|
|
}, 30);
|
|
} catch (_) {}
|
|
}
|
|
|
|
function placePoint(latlng) {
|
|
if (points.length >= 10) { alert('Maximum of 10 waypoints.'); return; }
|
|
var marker = L.marker(latlng, { draggable: true }).addTo(map);
|
|
markers.push(marker);
|
|
points.push(latlng);
|
|
originalPoints.push({ lat: latlng.lat, lng: latlng.lng });
|
|
marker.bindPopup(points.length === 1 ? 'Start' : 'End').openPopup();
|
|
marker.on('dragend', function(ev){
|
|
var m = ev.target;
|
|
var newLL = m.getLatLng();
|
|
var idx = markers.indexOf(m);
|
|
if (idx >= 0) {
|
|
points[idx] = { lat: newLL.lat, lng: newLL.lng };
|
|
originalPoints[idx] = { lat: newLL.lat, lng: newLL.lng };
|
|
renderWaypointList();
|
|
calculateRoute();
|
|
}
|
|
});
|
|
pulseAt(latlng);
|
|
if (points.length === 1) {
|
|
reverseGeocode(latlng.lat, latlng.lng, 'sourceInput');
|
|
} else if (points.length === 2) {
|
|
reverseGeocode(latlng.lat, latlng.lng, 'destInput');
|
|
}
|
|
calculateRoute();
|
|
renderWaypointList();
|
|
}
|
|
|
|
function isTouchDevice() {
|
|
return (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window);
|
|
}
|
|
|
|
var placeHandler = function(e) { placePoint(e.latlng); };
|
|
if (isTouchDevice()) {
|
|
map.on('click', placeHandler); // single tap on mobile
|
|
} else {
|
|
map.on('dblclick', placeHandler); // double click on desktop
|
|
}
|
|
|
|
// Listen for changes on all checkboxes
|
|
[
|
|
avoidHighwaysCheckbox,
|
|
useShortestCheckbox,
|
|
avoidTollRoadsCheckbox,
|
|
avoidFerriesCheckbox,
|
|
avoidUnpavedCheckbox
|
|
].forEach(function(checkbox) {
|
|
if (checkbox) {
|
|
checkbox.addEventListener('change', calculateRoute);
|
|
}
|
|
});
|
|
|
|
// Disable other checkboxes when "Use shortest route" is checked
|
|
useShortestCheckbox.addEventListener('change', function() {
|
|
var disable = useShortestCheckbox.checked;
|
|
[
|
|
avoidHighwaysCheckbox,
|
|
avoidTollRoadsCheckbox,
|
|
avoidFerriesCheckbox,
|
|
avoidUnpavedCheckbox
|
|
].forEach(function(cb) {
|
|
cb.disabled = disable;
|
|
});
|
|
});
|
|
|
|
// Adapted from Valhalla docs — polyline6 decoder for JS
|
|
function decodePolyline6(encoded) {
|
|
var coords = [];
|
|
var index = 0, lat = 0, lng = 0;
|
|
var shift = 0, result = 0, byte = null, latitude_change, longitude_change;
|
|
var factor = Math.pow(10, 6);
|
|
while (index < encoded.length) {
|
|
byte = shift = result = 0;
|
|
do {
|
|
byte = encoded.charCodeAt(index++) - 63;
|
|
result |= (byte & 0x1f) << shift;
|
|
shift += 5;
|
|
} while (byte >= 0x20);
|
|
latitude_change = (result & 1) ? ~(result >> 1) : (result >> 1);
|
|
|
|
shift = result = 0;
|
|
do {
|
|
byte = encoded.charCodeAt(index++) - 63;
|
|
result |= (byte & 0x1f) << shift;
|
|
shift += 5;
|
|
} while (byte >= 0x20);
|
|
longitude_change = (result & 1) ? ~(result >> 1) : (result >> 1);
|
|
|
|
lat += latitude_change;
|
|
lng += longitude_change;
|
|
|
|
coords.push([lat / factor, lng / factor]);
|
|
}
|
|
return coords;
|
|
}
|
|
|
|
// Remove Markers
|
|
function resetMarkers() {
|
|
markers.forEach(function(marker) {
|
|
map.removeLayer(marker);
|
|
});
|
|
markers = [];
|
|
points = [];
|
|
originalPoints = [];
|
|
if (routePolyline) {
|
|
map.removeLayer(routePolyline);
|
|
routePolyline = null;
|
|
}
|
|
if (snapLines && snapLines.length) {
|
|
snapLines.forEach(function(sl){ map.removeLayer(sl); });
|
|
snapLines = [];
|
|
}
|
|
if (waypointListEl) waypointListEl.innerHTML = '';
|
|
// Clear address fields
|
|
var sourceInput = document.getElementById('sourceInput');
|
|
var destInput = document.getElementById('destInput');
|
|
sourceInput.value = '';
|
|
destInput.value = '';
|
|
delete sourceInput.dataset.lat;
|
|
delete sourceInput.dataset.lon;
|
|
delete destInput.dataset.lat;
|
|
delete destInput.dataset.lon;
|
|
|
|
// Hide and clear directions
|
|
var directionsSheet = document.getElementById('directionsSheet');
|
|
var directionsList = document.getElementById('directionsList');
|
|
if (directionsSheet) directionsSheet.style.display = 'none';
|
|
if (directionsList) directionsList.innerHTML = '';
|
|
|
|
// Hide summary pill
|
|
var pill = document.getElementById('summaryPill');
|
|
if (pill) pill.classList.add('d-none');
|
|
}
|
|
|
|
// Make resetMarkers available globally
|
|
window.resetMarkers = resetMarkers;
|
|
|
|
// Plot Route button logic
|
|
document.getElementById('plotRouteBtn').addEventListener('click', function() {
|
|
var sourceInput = document.getElementById('sourceInput');
|
|
var destInput = document.getElementById('destInput');
|
|
var sourceLat = parseFloat(sourceInput.dataset.lat);
|
|
var sourceLon = parseFloat(sourceInput.dataset.lon);
|
|
var destLat = parseFloat(destInput.dataset.lat);
|
|
var destLon = parseFloat(destInput.dataset.lon);
|
|
|
|
if (!isNaN(sourceLat) && !isNaN(sourceLon) && !isNaN(destLat) && !isNaN(destLon)) {
|
|
// Remove old markers
|
|
markers.forEach(function(marker) {
|
|
map.removeLayer(marker);
|
|
});
|
|
markers = [];
|
|
points = [];
|
|
|
|
// Add new markers
|
|
var startMarker = L.marker([sourceLat, sourceLon]).addTo(map).bindPopup("Start").openPopup();
|
|
var endMarker = L.marker([destLat, destLon]).addTo(map).bindPopup("End").openPopup();
|
|
markers.push(startMarker, endMarker);
|
|
points.push({lat: sourceLat, lng: sourceLon}, {lat: destLat, lng: destLon});
|
|
|
|
calculateRoute();
|
|
} else {
|
|
alert("Please enter valid addresses for both start and destination.");
|
|
}
|
|
});
|
|
|
|
function reverseGeocode(lat, lon, inputId) {
|
|
fetch(`/reverse?format=json&lat=${lat}&lon=${lon}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
var input = document.getElementById(inputId);
|
|
if (!input) return;
|
|
input.value = (data && (data.display_name || (data.address && (data.address.road || data.address.city)))) || `${lat}, ${lon}`;
|
|
input.dataset.lat = lat;
|
|
input.dataset.lon = lon;
|
|
})
|
|
.catch(() => {
|
|
var input = document.getElementById(inputId);
|
|
if (!input) return;
|
|
input.value = `${lat}, ${lon}`;
|
|
input.dataset.lat = lat;
|
|
input.dataset.lon = lon;
|
|
});
|
|
}
|
|
|
|
function exportRouteAsGPX(latlngs) {
|
|
if (!latlngs || latlngs.length === 0) {
|
|
alert("No route to export.");
|
|
return;
|
|
}
|
|
const now = new Date().toISOString();
|
|
// Build <rte> using Valhalla maneuvers (snap to their begin shape indices)
|
|
var rtepts = '';
|
|
if (Array.isArray(lastRtePoints) && lastRtePoints.length) {
|
|
rtepts = lastRtePoints.map(function(p){
|
|
var safe = (p.name || 'Step').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
return ` <rtept lat="${p.lat}" lon="${p.lon}"><name>${safe}</name></rtept>`;
|
|
}).join('\n');
|
|
}
|
|
|
|
var mode = exportModeSel ? (exportModeSel.value || 'both') : 'both';
|
|
var parts = [];
|
|
parts.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
parts.push(`<gpx version="1.1" creator="FreeMoto" xmlns="http://www.topografix.com/GPX/1/1">`);
|
|
parts.push(` <metadata>\n <name>FreeMoto Route</name>\n <time>${now}</time>\n </metadata>`);
|
|
if (mode === 'route' || mode === 'both') {
|
|
parts.push(` <rte>\n <name>FreeMoto Directions</name>`);
|
|
parts.push(rtepts);
|
|
parts.push(` </rte>`);
|
|
}
|
|
if (mode === 'track' || mode === 'both') {
|
|
parts.push(` <trk>\n <name>FreeMoto Route</name>\n <type>motorcycle</type>\n <trkseg>`);
|
|
parts.push(latlngs.map(pt => ` <trkpt lat="${pt[0]}" lon="${pt[1]}"><time>${now}</time></trkpt>`).join('\n'));
|
|
parts.push(` </trkseg>\n </trk>`);
|
|
}
|
|
parts.push(`</gpx>`);
|
|
let gpx = parts.join('\n');
|
|
|
|
let blob = new Blob([gpx], {type: 'application/gpx+xml'});
|
|
let url = URL.createObjectURL(blob);
|
|
|
|
// iOS workaround: open in new tab instead of triggering download
|
|
if (navigator.userAgent.match(/(iPad|iPhone|iPod)/i)) {
|
|
window.open(url, '_blank');
|
|
} else {
|
|
let a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'freemoto-route.gpx';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
}
|
|
|
|
// Bind all GPX export buttons
|
|
document.querySelectorAll('.export-gpx').forEach(function(btn){
|
|
btn.addEventListener('click', function() {
|
|
if (routePolyline) {
|
|
let latlngs = routePolyline.getLatLngs().map(ll => [ll.lat, ll.lng]);
|
|
exportRouteAsGPX(latlngs);
|
|
} else {
|
|
alert('No route to export.');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Close directions sheet
|
|
var closeBtn = document.getElementById('closeDirections');
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', function() {
|
|
var sheet = document.getElementById('directionsSheet');
|
|
if (sheet) sheet.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
// Also hide summary pill when sheet closed manually
|
|
var pill = document.getElementById('summaryPill');
|
|
if (closeBtn && pill) {
|
|
closeBtn.addEventListener('click', function(){ pill.classList.add('d-none'); });
|
|
}
|
|
|
|
// Expose recalc for twistiness changes
|
|
window.recalculateRoute = function(){ calculateRoute(); };
|
|
window.createRoundTrip = function(){ createRoundTrip(); };
|
|
}); |