Files
freemoto/app/web/static/route.js
2025-09-17 10:39:49 +00:00

264 lines
10 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;
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 = `<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');
} else {
infoCard.innerHTML = `<strong>Route info unavailable.</strong>`;
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 =
`<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="FreeMoto" xmlns="http://www.topografix.com/GPX/1/1">
<trk>
<name>FreeMoto Route</name>
<trkseg>
${latlngs.map(pt => ` <trkpt lat="${pt[0]}" lon="${pt[1]}"></trkpt>`).join('\n')}
</trkseg>
</trk>
</gpx>`;
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.");
}
});
});