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; 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; } var costing_options = { motorcycle: moto }; var requestBody = { locations: [ { lat: points[0].lat, lon: points[0].lng }, { lat: points[1].lat, lon: points[1].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 leg = data.trip && data.trip.legs && data.trip.legs[0]; var infoCard = document.getElementById('routeInfoCard'); if (leg && leg.summary && typeof leg.summary.length === 'number' && typeof leg.summary.time === 'number') { var distanceKm = (leg.summary.length).toFixed(1); // already in km var durationMin = Math.round(leg.summary.time / 60); // seconds to minutes var info = `Distance: ${distanceKm} km
Estimated Time: ${durationMin} min
Motorcycle Profile`; infoCard.innerHTML = info; infoCard.classList.remove('d-none'); } else { infoCard.innerHTML = `Route info unavailable.`; infoCard.classList.remove('d-none'); console.log('Valhalla response:', data); } var latlngs = decodePolyline6(leg.shape); if (routePolyline) { map.removeLayer(routePolyline); } routePolyline = L.polyline(latlngs, { color: 'red', weight: 5}).addTo(map); map.fitBounds(routePolyline.getBounds()); }); } } map.on('click', function(e) { if (points.length < 2) { var marker = L.marker(e.latlng).addTo(map); markers.push(marker); points.push(e.latlng); marker.bindPopup(points.length === 1 ? "Start" : "End").openPopup(); // Reverse geocode and fill address field if (points.length === 1) { reverseGeocode(e.latlng.lat, e.latlng.lng, 'sourceInput'); } else if (points.length === 2) { reverseGeocode(e.latlng.lat, e.latlng.lng, 'destInput'); } } calculateRoute(); }); // 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 = []; if (routePolyline) { map.removeLayer(routePolyline); routePolyline = null; } // 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; } // 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 (data && data.address) { // Use the same format as your autocomplete input.value = formatAddress(data); } else { 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; } let gpx = ` FreeMoto Route ${latlngs.map(pt => ` `).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); } document.getElementById('exportGpxBtn').addEventListener('click', function() { if (routePolyline) { // routePolyline.getLatLngs() returns array of LatLng objects let latlngs = routePolyline.getLatLngs().map(ll => [ll.lat, ll.lng]); exportRouteAsGPX(latlngs); } else { alert("No route to export."); } }); });