Rewrote UI, added option to export as gpx

This commit is contained in:
Pedan
2025-07-29 17:16:14 +02:00
parent 05a8d7abbd
commit 24e735e113
3 changed files with 214 additions and 65 deletions

View File

@@ -27,6 +27,26 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
function formatAddress(item) {
if (!item.address) return item.display_name;
const addr = item.address;
let street = addr.road || addr.pedestrian || addr.cycleway || addr.footway || addr.street || '';
let number = addr.house_number || '';
let postcode = addr.postcode || '';
let city = addr.city || addr.town || addr.village || addr.hamlet || addr.municipality || addr.county || '';
// Build formatted string: Street Number, Postal code City
let address = '';
if (street) address += street;
if (number) address += (address ? ' ' : '') + number;
if (postcode || city) {
address += (address ? ', ' : '');
address += postcode;
if (postcode && city) address += ' ';
address += city;
}
return address;
}
function showSuggestions(inputId, suggestionsId, value) {
var suggestionsBox = document.getElementById(suggestionsId);
if (!value) {
@@ -34,7 +54,7 @@ document.addEventListener('DOMContentLoaded', function() {
suggestionsBox.style.display = 'none';
return;
}
fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(value))
fetch('https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&q=' + encodeURIComponent(value))
.then(response => response.json())
.then(data => {
suggestionsBox.innerHTML = '';
@@ -43,10 +63,11 @@ document.addEventListener('DOMContentLoaded', function() {
var option = document.createElement('button');
option.type = 'button';
option.className = 'list-group-item list-group-item-action';
option.textContent = item.display_name;
option.textContent = formatAddress(item);
option.title = item.display_name; // Show full address on hover
option.onclick = function() {
var input = document.getElementById(inputId);
input.value = item.display_name;
input.value = formatAddress(item);
input.dataset.lat = item.lat;
input.dataset.lon = item.lon;
suggestionsBox.innerHTML = '';

View File

@@ -1,10 +1,9 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>FreeMoto Web</title>
<title>FreeMoto Navigation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<style>
@@ -18,33 +17,34 @@
min-width: 100vw;
z-index: 1;
}
.controls-container {
position: absolute;
top: 24px;
left: 24px;
.nav-panel {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
max-width: 100vw;
z-index: 1001;
background: rgba(255,255,255,0.98);
border-radius: 18px;
box-shadow: 0 2px 16px rgba(0,0,0,0.18);
padding: 28px 32px 22px 32px;
min-width: 300px;
max-width: 370px;
border-radius: 18px 18px 0 0;
box-shadow: 0 -2px 16px rgba(0,0,0,0.18);
padding: 12px 8px 8px 8px;
overflow-y: auto;
transition: box-shadow 0.2s;
}
.leaflet-control-attribution {
z-index: 1002;
@media (min-width: 600px) {
.nav-panel {
left: 24px;
width: 370px;
max-width: 420px;
border-radius: 18px;
top: 24px;
bottom: auto;
box-shadow: 0 2px 16px rgba(0,0,0,0.18);
}
}
.input-group-text {
background: #f8f9fa;
}
.section-title {
#routeInfoCard {
margin-bottom: 1rem;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
margin-top: 1rem;
color: #495057;
}
hr {
margin: 1.2rem 0;
}
#sourceSuggestions,
#destSuggestions {
@@ -58,64 +58,109 @@
border-radius: 0 0 0.5rem 0.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
margin-top: 1rem;
color: #495057;
}
.card-icon {
font-size: 2rem;
margin-right: 0.5rem;
vertical-align: middle;
}
.btn-lg, .form-control {
font-size: 1.2rem;
padding: 0.7rem 1rem;
}
.form-check-label {
font-size: 1rem;
}
.input-group-text svg,
.btn svg {
vertical-align: middle;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="controls-container shadow-lg">
<h4 class="mb-3 text-primary">FreeMoto Route Planner</h4>
<div class="mb-3">
<div class="nav-panel shadow-lg">
<div class="text-center mb-2">
<span class="card-icon">🏍️</span>
<span class="fs-4 fw-bold text-primary">FreeMoto</span>
</div>
<div id="routeInfoCard" class="alert alert-info d-none" role="alert"></div>
<div class="mb-2">
<div class="section-title">Route</div>
<div class="input-group mb-2 position-relative">
<span class="input-group-text" title="Start"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0d6efd" class="bi bi-geo-alt" viewBox="0 0 16 16"><path d="M8 16s6-5.686 6-10A6 6 0 1 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></svg></span>
<input type="text" class="form-control" id="sourceInput" placeholder="Enter start address" autocomplete="off">
<span class="input-group-text" title="Start">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#0d6efd" class="bi bi-geo-alt" viewBox="0 0 16 16"><path d="M8 16s6-5.686 6-10A6 6 0 1 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></svg>
</span>
<input type="text" class="form-control" id="sourceInput" placeholder="Start address" autocomplete="off">
<button class="btn btn-outline-secondary" type="button" id="useCurrentSource" title="Use current location">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
</button>
<div id="sourceSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
</div>
<div class="input-group mb-2 position-relative">
<span class="input-group-text" title="Destination"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#dc3545" class="bi bi-flag" viewBox="0 0 16 16"><path d="M14.778 2.222a.5.5 0 0 1 0 .707l-2.5 2.5a.5.5 0 0 1-.707 0l-2.5-2.5a.5.5 0 0 1 .707-.707L12 3.793l2.071-2.071a.5.5 0 0 1 .707 0z"/><path d="M2.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5z"/></svg></span>
<input type="text" class="form-control" id="destInput" placeholder="Enter destination address" autocomplete="off">
<span class="input-group-text" title="Destination">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#dc3545" class="bi bi-flag" viewBox="0 0 16 16"><path d="M14.778 2.222a.5.5 0 0 1 0 .707l-2.5 2.5a.5.5 0 0 1-.707 0l-2.5-2.5a.5.5 0 0 1 .707-.707L12 3.793l2.071-2.071a.5.5 0 0 1 .707 0z"/><path d="M2.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5z"/></svg>
</span>
<input type="text" class="form-control" id="destInput" placeholder="Destination address" autocomplete="off">
<button class="btn btn-outline-secondary" type="button" id="useCurrentDest" title="Use current location">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
</button>
<div id="destSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
</div>
<div class="d-grid gap-2 mb-3">
<button type="button" id="plotRouteBtn" class="btn btn-success btn-sm">Plot Route</button>
<div class="d-grid gap-2 mb-2">
<button type="button" id="plotRouteBtn" class="btn btn-success btn-lg">Plot Route</button>
</div>
</div>
<hr>
<div class="section-title">Route Options</div>
<form>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="avoidHighways">
<label class="form-check-label" for="avoidHighways">Avoid freeways</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="useShortest">
<label class="form-check-label" for="useShortest">Use shortest route</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="avoidTollRoads">
<label class="form-check-label" for="avoidTollRoads">Avoid toll roads</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="avoidFerries">
<label class="form-check-label" for="avoidFerries">Avoid ferries</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="avoidUnpaved">
<label class="form-check-label" for="avoidUnpaved">Avoid unpaved roads</label>
<div class="row g-2">
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="avoidHighways">
<label class="form-check-label" for="avoidHighways">Avoid freeways</label>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="useShortest">
<label class="form-check-label" for="useShortest">Shortest route</label>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="avoidTollRoads">
<label class="form-check-label" for="avoidTollRoads">Avoid tolls</label>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="avoidFerries">
<label class="form-check-label" for="avoidFerries">Avoid ferries</label>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="avoidUnpaved">
<label class="form-check-label" for="avoidUnpaved">Avoid unpaved</label>
</div>
</div>
</div>
</form>
<div class="d-grid gap-2 mb-3">
<button onclick="resetMarkers()" class="btn btn-primary btn-sm">Reset Points</button>
<div class="d-flex justify-content-between mt-3 mb-2">
<button onclick="resetMarkers()" class="btn btn-primary btn-lg flex-fill me-2">Reset Points</button>
<div class="btn-group flex-fill" role="group" aria-label="Zoom controls">
<button type="button" class="btn btn-outline-secondary btn-lg" id="zoomInBtn" title="Zoom in">+</button>
<button type="button" class="btn btn-outline-secondary btn-lg" id="zoomOutBtn" title="Zoom out"></button>
</div>
</div>
<div class="btn-group mb-2 w-100" role="group" aria-label="Zoom controls">
<button type="button" class="btn btn-outline-secondary btn-sm" id="zoomInBtn" title="Zoom in">+</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="zoomOutBtn" title="Zoom out"></button>
<div class="d-grid gap-2 mb-2">
<button type="button" id="exportGpxBtn" class="btn btn-warning btn-lg">Export GPX</button>
</div>
</div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>

View File

@@ -47,7 +47,22 @@ document.addEventListener('DOMContentLoaded', function() {
})
.then(response => response.json())
.then(data => {
var latlngs = decodePolyline6(data.trip.legs[0].shape);
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 / 1000).toFixed(1); // meters to 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);
}
@@ -63,6 +78,13 @@ document.addEventListener('DOMContentLoaded', function() {
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();
});
@@ -135,6 +157,15 @@ document.addEventListener('DOMContentLoaded', function() {
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
@@ -168,4 +199,56 @@ document.addEventListener('DOMContentLoaded', function() {
alert("Please enter valid addresses for both start and destination.");
}
});
function reverseGeocode(lat, lon, inputId) {
fetch(`https://nominatim.openstreetmap.org/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);
let a = document.createElement('a');
a.href = url;
a.download = "freemoto-route.gpx";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
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.");
}
});
});