Some checks failed
build-and-push / docker (push) Failing after 10m2s
- UI (neutral minimal, glass-like)
- Rewrote app shell and control panel in [app/web/static/index.html](cci:7://file:///home/pedan/freemoto/app/web/static/index.html:0:0-0:0)
- Modernized responsive styles in [app/web/static/styles.css](cci:7://file:///home/pedan/freemoto/app/web/static/styles.css:0:0-0:0)
- Proper internal scrolling for control panel, suggestions, waypoints, and directions sheet
- Improved dark mode, spacing, and visual polish
- Round Trip generation
- Direction preference (N/E/S/W) with biased waypoint bearings ([route.js](cci:7://file:///home/pedan/freemoto/app/web/static/route.js:0:0-0:0))
- Ferry avoidance by default on round trips; added strong ferry cost
- Coastal safety: reverse-geocode water check and automatic inland adjustment for generated waypoints
- New “Scenic optimizer” option: generate candidate loops, route them, score twistiness, pick the best
- Heading-change/turn-density based scoring using decoded polyline
- Isochrone-guided loops
- Backend: added `/isochrone` proxy in [app/web/main.go](cci:7://file:///home/pedan/freemoto/app/web/main.go:0:0-0:0) (derives Valhalla base from `VALHALLA_URL`)
- Frontend: optional “Use isochrone guidance” + time (minutes); sample preferred sector of polygon
- Settings persistence and live updates
- Direction, scenic, isochrone, and distance persist via localStorage
- Changing options regenerates round trips when enabled
- Docker/Build
- Confirmed multi-arch build (amd64/arm64) and push to git.ztsw.de/pedan/freemoto/freemoto-web:latest
Refs: [index.html](cci:7://file:///home/pedan/freemoto/app/web/static/index.html:0:0-0:0), [styles.css](cci:7://file:///home/pedan/freemoto/app/web/static/styles.css:0:0-0:0), [route.js](cci:7://file:///home/pedan/freemoto/app/web/static/route.js:0:0-0:0), [main.js](cci:7://file:///home/pedan/freemoto/app/web/static/main.js:0:0-0:0), [app/web/main.go](cci:7://file:///home/pedan/freemoto/app/web/main.go:0:0-0:0), [Dockerfile](cci:7://file:///home/pedan/freemoto/Dockerfile:0:0-0:0)
317 lines
13 KiB
JavaScript
317 lines
13 KiB
JavaScript
// Center on a default point
|
|
var map = L.map('map', { zoomControl: false, doubleClickZoom: false }).setView([53.866237, 10.676289], 18);
|
|
|
|
// Add OSM tiles
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
}).addTo(map);
|
|
|
|
var userIcon = L.icon({
|
|
iconUrl: '/maps-arrow.svg', // Add a motorcycle icon to your static folder
|
|
iconSize: [40, 40]
|
|
});
|
|
|
|
// Get users location
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(function(position) {
|
|
var lat = position.coords.latitude;
|
|
var lon = position.coords.longitude;
|
|
map.setView([lat, lon], 14);
|
|
L.marker([lat, lon], {icon: userIcon}).addTo(map).bindPopup('You are here!');
|
|
});
|
|
}
|
|
|
|
// Custom Bootstrap zoom controls
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var zoomInBtn = document.getElementById('zoomInBtn');
|
|
var zoomOutBtn = document.getElementById('zoomOutBtn');
|
|
var themeToggle = document.getElementById('themeToggle');
|
|
var clearRouteBtn = document.getElementById('clearRouteBtn');
|
|
var twistiness = document.getElementById('twistiness');
|
|
var twistinessValue = document.getElementById('twistinessValue');
|
|
var highwayPref = document.getElementById('highwayPref');
|
|
var highwayPrefValue = document.getElementById('highwayPrefValue');
|
|
var exportModeSel = document.getElementById('exportMode');
|
|
var voiceToggle = document.getElementById('voiceToggle');
|
|
var roundTripToggle = document.getElementById('roundTripToggle');
|
|
var roundTripKm = document.getElementById('roundTripKm');
|
|
var roundTripBtn = document.getElementById('roundTripBtn');
|
|
var roundTripNoRepeat = document.getElementById('roundTripNoRepeat');
|
|
var roundTripDir = document.getElementById('roundTripDir');
|
|
var roundTripScenic = document.getElementById('roundTripScenic');
|
|
var roundTripIsochrone = document.getElementById('roundTripIsochrone');
|
|
var isochroneMinutes = document.getElementById('isochroneMinutes');
|
|
if (zoomInBtn && zoomOutBtn) {
|
|
zoomInBtn.addEventListener('click', function() {
|
|
map.zoomIn();
|
|
});
|
|
zoomOutBtn.addEventListener('click', function() {
|
|
map.zoomOut();
|
|
});
|
|
}
|
|
|
|
// Theme toggle with persistence (Dracula-like)
|
|
function applyTheme(theme) {
|
|
if (theme === 'dark') {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
if (themeToggle) themeToggle.textContent = '☀️';
|
|
} else {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
if (themeToggle) themeToggle.textContent = '🌙';
|
|
}
|
|
}
|
|
var savedTheme = localStorage.getItem('freemoto-theme');
|
|
if (!savedTheme || savedTheme === 'auto') {
|
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
applyTheme(prefersDark ? 'dark' : 'light');
|
|
if (!savedTheme) localStorage.setItem('freemoto-theme', 'auto');
|
|
} else {
|
|
applyTheme(savedTheme);
|
|
}
|
|
if (themeToggle) {
|
|
themeToggle.addEventListener('click', function() {
|
|
var next = (localStorage.getItem('freemoto-theme') || 'light') === 'dark' ? 'light' : 'dark';
|
|
localStorage.setItem('freemoto-theme', next);
|
|
applyTheme(next);
|
|
});
|
|
}
|
|
|
|
// Clear route button
|
|
if (clearRouteBtn && typeof window.resetMarkers === 'function') {
|
|
clearRouteBtn.addEventListener('click', function() {
|
|
window.resetMarkers();
|
|
});
|
|
}
|
|
|
|
// Twistiness slider
|
|
function applyTwistiness(val) {
|
|
if (twistinessValue) twistinessValue.textContent = String(val);
|
|
localStorage.setItem('freemoto-twistiness', String(val));
|
|
}
|
|
var savedTwist = parseInt(localStorage.getItem('freemoto-twistiness') || '50', 10);
|
|
if (!isNaN(savedTwist) && twistiness) {
|
|
twistiness.value = savedTwist;
|
|
applyTwistiness(savedTwist);
|
|
}
|
|
if (twistiness) {
|
|
twistiness.addEventListener('input', function(e){
|
|
var v = parseInt(e.target.value, 10);
|
|
applyTwistiness(v);
|
|
});
|
|
twistiness.addEventListener('change', function(){
|
|
if (typeof window.recalculateRoute === 'function') {
|
|
window.recalculateRoute();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Highway preference slider (0..100 -> 0..1 use_highways)
|
|
function applyHighwayPref(val) {
|
|
if (highwayPrefValue) highwayPrefValue.textContent = String(val);
|
|
localStorage.setItem('freemoto-highways', String(val));
|
|
}
|
|
var savedHigh = parseInt(localStorage.getItem('freemoto-highways') || '50', 10);
|
|
if (!isNaN(savedHigh) && highwayPref) {
|
|
highwayPref.value = savedHigh;
|
|
applyHighwayPref(savedHigh);
|
|
}
|
|
if (highwayPref) {
|
|
highwayPref.addEventListener('input', function(e){
|
|
var v = parseInt(e.target.value, 10);
|
|
applyHighwayPref(v);
|
|
});
|
|
highwayPref.addEventListener('change', function(){
|
|
if (typeof window.recalculateRoute === 'function') {
|
|
window.recalculateRoute();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Persist export mode
|
|
if (exportModeSel) {
|
|
var savedMode = localStorage.getItem('freemoto-export-mode') || 'both';
|
|
exportModeSel.value = savedMode;
|
|
exportModeSel.addEventListener('change', function(){
|
|
localStorage.setItem('freemoto-export-mode', exportModeSel.value);
|
|
});
|
|
}
|
|
|
|
// Persist voice toggle
|
|
if (voiceToggle) {
|
|
var savedVoice = localStorage.getItem('freemoto-voice') === '1';
|
|
voiceToggle.checked = savedVoice;
|
|
voiceToggle.addEventListener('change', function(){
|
|
localStorage.setItem('freemoto-voice', voiceToggle.checked ? '1' : '0');
|
|
});
|
|
}
|
|
|
|
// Round Trip settings persistence
|
|
if (roundTripToggle) {
|
|
var savedRT = localStorage.getItem('freemoto-roundtrip') === '1';
|
|
roundTripToggle.checked = savedRT;
|
|
roundTripToggle.addEventListener('change', function(){
|
|
localStorage.setItem('freemoto-roundtrip', roundTripToggle.checked ? '1' : '0');
|
|
});
|
|
}
|
|
if (roundTripKm) {
|
|
var savedKm = parseInt(localStorage.getItem('freemoto-roundtrip-km') || '100', 10);
|
|
if (!isNaN(savedKm)) roundTripKm.value = savedKm;
|
|
roundTripKm.addEventListener('change', function(){
|
|
var v = parseInt(roundTripKm.value, 10);
|
|
if (!isNaN(v)) localStorage.setItem('freemoto-roundtrip-km', String(v));
|
|
if (roundTripToggle && roundTripToggle.checked && typeof window.createRoundTrip === 'function') {
|
|
window.createRoundTrip();
|
|
}
|
|
});
|
|
}
|
|
if (roundTripBtn) {
|
|
roundTripBtn.addEventListener('click', function(){
|
|
if (typeof window.createRoundTrip === 'function') {
|
|
window.createRoundTrip();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (roundTripNoRepeat) {
|
|
var savedNR = localStorage.getItem('freemoto-roundtrip-norepeat') === '1';
|
|
roundTripNoRepeat.checked = savedNR;
|
|
roundTripNoRepeat.addEventListener('change', function(){
|
|
localStorage.setItem('freemoto-roundtrip-norepeat', roundTripNoRepeat.checked ? '1' : '0');
|
|
});
|
|
}
|
|
|
|
// Direction preference persistence and live update
|
|
if (roundTripDir) {
|
|
var savedDir = localStorage.getItem('freemoto-roundtrip-dir') || 'any';
|
|
roundTripDir.value = savedDir;
|
|
roundTripDir.addEventListener('change', function(){
|
|
localStorage.setItem('freemoto-roundtrip-dir', roundTripDir.value);
|
|
if (roundTripToggle && roundTripToggle.checked && typeof window.createRoundTrip === 'function') {
|
|
// Regenerate the round trip to reflect new direction
|
|
window.createRoundTrip();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Scenic optimizer toggle
|
|
if (roundTripScenic) {
|
|
var savedScenic = localStorage.getItem('freemoto-roundtrip-scenic') === '1';
|
|
roundTripScenic.checked = savedScenic;
|
|
roundTripScenic.addEventListener('change', function(){
|
|
localStorage.setItem('freemoto-roundtrip-scenic', roundTripScenic.checked ? '1' : '0');
|
|
if (roundTripToggle && roundTripToggle.checked && typeof window.createRoundTrip === 'function') {
|
|
window.createRoundTrip();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Isochrone toggle and minutes
|
|
if (roundTripIsochrone) {
|
|
var savedIso = localStorage.getItem('freemoto-roundtrip-isochrone') === '1';
|
|
roundTripIsochrone.checked = savedIso;
|
|
roundTripIsochrone.addEventListener('change', function(){
|
|
localStorage.setItem('freemoto-roundtrip-isochrone', roundTripIsochrone.checked ? '1' : '0');
|
|
if (roundTripToggle && roundTripToggle.checked && typeof window.createRoundTrip === 'function') {
|
|
window.createRoundTrip();
|
|
}
|
|
});
|
|
}
|
|
if (isochroneMinutes) {
|
|
var savedMin = parseInt(localStorage.getItem('freemoto-isochrone-minutes') || '60', 10);
|
|
if (!isNaN(savedMin)) isochroneMinutes.value = savedMin;
|
|
isochroneMinutes.addEventListener('change', function(){
|
|
var v = parseInt(isochroneMinutes.value, 10);
|
|
if (!isNaN(v)) localStorage.setItem('freemoto-isochrone-minutes', String(v));
|
|
if (roundTripToggle && roundTripToggle.checked && typeof window.createRoundTrip === 'function') {
|
|
window.createRoundTrip();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Swap start/end like Google Maps
|
|
(function(){
|
|
var swapBtn = document.getElementById('swapBtn');
|
|
if (!swapBtn) return;
|
|
function swapInputs() {
|
|
var s = document.getElementById('sourceInput');
|
|
var d = document.getElementById('destInput');
|
|
if (!s || !d) return;
|
|
// Swap visible values
|
|
var tmpVal = s.value; s.value = d.value; d.value = tmpVal;
|
|
// Swap lat/lon datasets
|
|
var sLat = s.dataset.lat, sLon = s.dataset.lon;
|
|
s.dataset.lat = d.dataset.lat || '';
|
|
s.dataset.lon = d.dataset.lon || '';
|
|
d.dataset.lat = sLat || '';
|
|
d.dataset.lon = sLon || '';
|
|
// If both look valid, trigger plot
|
|
var sourceLat = parseFloat(s.dataset.lat);
|
|
var sourceLon = parseFloat(s.dataset.lon);
|
|
var destLat = parseFloat(d.dataset.lat);
|
|
var destLon = parseFloat(d.dataset.lon);
|
|
if (!isNaN(sourceLat) && !isNaN(sourceLon) && !isNaN(destLat) && !isNaN(destLon)) {
|
|
var plot = document.getElementById('plotRouteBtn');
|
|
if (plot) plot.click();
|
|
}
|
|
}
|
|
swapBtn.addEventListener('click', swapInputs);
|
|
})();
|
|
|
|
// Recenter FAB behavior
|
|
(function(){
|
|
var recenter = document.getElementById('recenterBtn');
|
|
if (!recenter) return;
|
|
recenter.addEventListener('click', function(){
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(function(position){
|
|
var lat = position.coords.latitude;
|
|
var lon = position.coords.longitude;
|
|
map.setView([lat, lon], Math.max(map.getZoom(), 14));
|
|
});
|
|
}
|
|
});
|
|
})();
|
|
|
|
// Mobile panel toggle (portrait)
|
|
try {
|
|
var panel = document.querySelector('.nav-panel');
|
|
var toggleBtn = document.getElementById('panelToggle');
|
|
function isSmallPortrait() {
|
|
var mqW = window.matchMedia('(max-width: 576px)');
|
|
var mqP = window.matchMedia('(orientation: portrait)');
|
|
return (mqW.matches && mqP.matches);
|
|
}
|
|
// Initialize collapsed state on load for small portrait
|
|
if (panel && isSmallPortrait()) {
|
|
panel.classList.add('collapsed');
|
|
if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false');
|
|
}
|
|
// Auto-collapse panel when user hits Plot on small portrait
|
|
var plotBtn = document.getElementById('plotRouteBtn');
|
|
if (plotBtn) {
|
|
plotBtn.addEventListener('click', function(){
|
|
if (panel && isSmallPortrait()) {
|
|
panel.classList.add('collapsed');
|
|
if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
}
|
|
if (toggleBtn && panel) {
|
|
toggleBtn.addEventListener('click', function() {
|
|
panel.classList.toggle('collapsed');
|
|
var expanded = !panel.classList.contains('collapsed');
|
|
toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
});
|
|
}
|
|
// When orientation/viewport changes, ensure panel doesn't exceed screen
|
|
window.addEventListener('resize', function(){
|
|
if (!panel) return;
|
|
if (isSmallPortrait()) {
|
|
// Keep collapsed if it would cover map too much
|
|
if (!panel.classList.contains('collapsed')) panel.classList.add('collapsed');
|
|
if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
} catch (_) {}
|
|
}); |