Routenplanung eingebaut

This commit is contained in:
2026-05-23 20:53:16 +02:00
parent b24996e67f
commit 91fb1fcf0b
6 changed files with 1877 additions and 0 deletions
+146
View File
@@ -172,6 +172,152 @@ header {
.ctrl-select:focus { border-color:rgba(0,255,145,.5); }
.ctrl-status { font-size:7px; letter-spacing:1.5px; color:rgba(0,255,145,.45); white-space:nowrap; min-width:50px; text-align:right; }
.act-btn.is_active {
background: rgba(0,136,255,.16);
color: #55aaff;
border-color: rgba(0,136,255,.55);
box-shadow: 0 0 14px rgba(0,136,255,.18);
}
.admin-section-grid_map {
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.85fr);
}
.admin-col_map_main,
.admin-col_map_side {
min-width: 0;
}
.map-panel {
display: flex;
flex-direction: column;
min-height: 620px;
}
.map-toolbar {
display: flex;
gap: 8px;
padding: 10px 12px 0;
flex-wrap: wrap;
}
.map-copy {
padding: 10px 12px;
}
.map-stage {
position: relative;
flex: 1;
min-height: 460px;
margin: 0 12px 12px;
border: 1px solid rgba(0,255,145,.12);
background:
radial-gradient(circle at 20% 20%, rgba(0,136,255,.12), transparent 34%),
radial-gradient(circle at 78% 24%, rgba(0,255,145,.08), transparent 28%),
#04101a;
overflow: hidden;
}
.rover-map,
.rover-map-fallback {
position: absolute;
inset: 0;
}
.rover-map-fallback {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px;
color: rgba(0,255,145,.65);
font-size: 12px;
letter-spacing: .6px;
background: rgba(4, 16, 26, .92);
}
.map-waypoint-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px 12px;
}
.map-waypoint-empty {
padding: 12px;
border: 1px dashed rgba(0,255,145,.14);
color: rgba(0,255,145,.42);
font-size: 8px;
letter-spacing: 1px;
text-transform: uppercase;
text-align: center;
}
.map-waypoint-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border: 1px solid rgba(0,255,145,.1);
background: rgba(0,255,145,.03);
}
.map-waypoint-index {
min-width: 42px;
font-size: 8px;
color: rgba(0,255,145,.5);
letter-spacing: 1.4px;
text-transform: uppercase;
}
.map-waypoint-coords {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.map-waypoint-coords strong,
.map-waypoint-coords span {
font-size: 10px;
letter-spacing: .5px;
color: rgba(0,255,145,.82);
}
.leaflet-container {
font-family: var(--mono);
background: #04101a;
}
.leaflet-control-attribution {
background: rgba(4, 16, 26, .82);
color: rgba(0,255,145,.58);
}
.leaflet-control-attribution a {
color: rgba(85,170,255,.85);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: #07111c;
color: rgba(0,255,145,.82);
}
@media (max-width: 1024px) {
.admin-section-grid_map {
grid-template-columns: 1fr;
}
.map-panel {
min-height: 0;
}
.map-stage {
min-height: 360px;
}
}
/* ── AUTH OVERLAY ───────────────────────────────── */
.auth-overlay {
position:fixed; inset:0; background:rgba(5,8,15,.93);
+652
View File
@@ -5,7 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CUNO // Admin</title>
<link rel="stylesheet" href="./app.css?v=20260522104500">
<link rel="stylesheet" href="./vendor/leaflet/leaflet.css">
<script src="./vendor/roslib.min.js"></script>
<script src="./vendor/leaflet/leaflet.js"></script>
</head>
<body class="page-admin">
@@ -40,6 +42,7 @@
<div class="admin-section-tabs" role="tablist" aria-label="Admin-Bereiche">
<button id="admin_tab_system" class="admin-section-tab is_active" role="tab" aria-selected="true" onclick="switchAdminSection('system')">Admin</button>
<button id="admin_tab_drive" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('drive')">Geschwindigkeit / Weg</button>
<button id="admin_tab_map" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('map')">Karte</button>
<button id="admin_tab_motor" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('motor')">Motorentest</button>
</div>
@@ -245,6 +248,115 @@
</div>
</section>
<section id="admin_panel_map" class="admin-section-panel" role="tabpanel">
<div class="admin-section-grid admin-section-grid_map">
<div class="admin-col admin-col_map_main">
<div class="panel map-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Rover-Karte</span>
<span class="p-tag">Live Position</span>
</div>
<div class="map-toolbar">
<button id="map_plan_toggle" class="act-btn subtle is_active" onclick="toggleWaypointPlanning()">Route planen: an</button>
<button class="act-btn" onclick="addWaypointAtMapCenter()">Wegpunkt an Mitte setzen</button>
<button class="act-btn warm" onclick="centerMapOnRover()">Auf Rover zentrieren</button>
<button class="act-btn subtle" onclick="clearPlannedRoute()">Route löschen</button>
</div>
<div class="copy-text map-copy">
Die Karte zeigt den aktuellen GPS-Standort des Rovers. Ein Klick in die Karte setzt sofort einen Wegpunkt, solange <code>Route planen</code> aktiv ist. Die eigentliche Routenausführung binden wir später darauf auf.
</div>
<div class="map-stage">
<div id="rover_map" class="rover-map"></div>
<div id="rover_map_fallback" class="rover-map-fallback map-hidden" hidden>
Kartenbibliothek nicht geladen. GPS-Daten sind trotzdem unten sichtbar.
</div>
</div>
</div>
</div>
<div class="admin-col admin-col_map_side">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">GPS</span>
<span class="p-tag">Telemetry</span>
</div>
<div class="sr">
<span class="sr-led led-ok"></span>
<span class="sr-key">Status</span>
<span id="map_gps_status" class="sr-val">Offline</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Sat</span>
<span id="map_gps_satellites" class="sr-val">--</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Breite</span>
<span id="map_gps_lat" class="sr-val">--</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Länge</span>
<span id="map_gps_lon" class="sr-val">--</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Höhe</span>
<span id="map_gps_alt" class="sr-val">--</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Genauigk.</span>
<span id="map_gps_accuracy" class="sr-val">--</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Letztes Fix</span>
<span id="map_gps_last_fix" class="sr-val">--</span>
</div>
</div>
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Route</span>
<span class="p-tag">Planning Scaffold</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Planung</span>
<span id="map_route_mode" class="sr-val">aus</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Wegpunkte</span>
<span id="map_waypoint_count" class="sr-val">0</span>
</div>
<div class="copy-text map-copy">
Diese Wegpunkte sind aktuell nur lokal in der Admin-Seite. Sie dienen als Vorbereitung für die spätere echte Routenprogrammierung.
</div>
<div class="map-route-storage">
<label class="map-route-storage-label" for="map_route_name">Name</label>
<input id="map_route_name" class="map-route-storage-input" type="text" placeholder="z. B. Hofrunde">
<button class="act-btn" onclick="saveCurrentRoute()">Route speichern</button>
</div>
<div class="map-route-storage map-route-storage_secondary">
<label class="map-route-storage-label" for="map_route_select">Gespeichert</label>
<select id="map_route_select" class="ctrl-select" onchange="loadSelectedRoute()">
<option value="">Route wählen</option>
</select>
<button class="act-btn subtle" onclick="refreshRouteLibrary()">Neu laden</button>
<button class="act-btn warm" onclick="deleteSelectedRoute()">Route löschen</button>
</div>
<div id="map_waypoint_list" class="map-waypoint-list"></div>
</div>
</div>
</div>
</section>
<section id="admin_panel_motor" class="admin-section-panel motor-admin-tab" role="tabpanel">
<div class="admin-section-grid">
<div class="admin-col">
@@ -365,6 +477,8 @@
var ros = null;
var roverCommandTopic = null;
var motorCommandsTopic = null;
var gpsFixTopic = null;
var gpsStatusTopic = null;
var isMotorTestRunning = false;
var isContainerActionRunning = false;
@@ -412,6 +526,7 @@
calibrationPoints: [],
savedCalibrationPoints: []
};
var routeLibrary = [];
var driveCalibrationMeasurement = {
distanceM: 1.0,
activeIndex: null,
@@ -423,6 +538,28 @@
startThresholdPercent: 2,
stopThresholdPercent: 1
};
var roverMapState = {
map: null,
mapReady: false,
roverMarker: null,
roverAccuracyCircle: null,
roverTrackLine: null,
routeLine: null,
waypointMarkers: [],
waypointPlanning: true,
waypoints: [],
trackPoints: [],
roverLat: null,
roverLon: null,
roverAlt: null,
accuracyMeters: null,
hasFix: false,
gpsStatusText: 'Offline',
satellites: '--',
lastFixAt: null,
hasCenteredOnFix: false,
suppressMapClickUntil: 0
};
function submitPassword() {
var input = document.getElementById('auth_input');
@@ -449,22 +586,35 @@
activeAdminSection = sectionName;
var systemTab = document.getElementById('admin_tab_system');
var driveTab = document.getElementById('admin_tab_drive');
var mapTab = document.getElementById('admin_tab_map');
var motorTab = document.getElementById('admin_tab_motor');
var systemPanel = document.getElementById('admin_panel_system');
var drivePanel = document.getElementById('admin_panel_drive');
var mapPanel = document.getElementById('admin_panel_map');
var motorPanel = document.getElementById('admin_panel_motor');
var isSystem = sectionName === 'system';
var isDrive = sectionName === 'drive';
var isMap = sectionName === 'map';
var isMotor = sectionName === 'motor';
systemTab.classList.toggle('is_active', isSystem);
driveTab.classList.toggle('is_active', isDrive);
mapTab.classList.toggle('is_active', isMap);
motorTab.classList.toggle('is_active', isMotor);
systemTab.setAttribute('aria-selected', isSystem ? 'true' : 'false');
driveTab.setAttribute('aria-selected', isDrive ? 'true' : 'false');
mapTab.setAttribute('aria-selected', isMap ? 'true' : 'false');
motorTab.setAttribute('aria-selected', isMotor ? 'true' : 'false');
systemPanel.classList.toggle('is_active', isSystem);
drivePanel.classList.toggle('is_active', isDrive);
mapPanel.classList.toggle('is_active', isMap);
motorPanel.classList.toggle('is_active', isMotor);
window.location.hash = isSystem ? '' : ('#' + sectionName);
if (isMap) {
initRoverMap();
window.setTimeout(function() {
if (roverMapState.map) roverMapState.map.invalidateSize();
}, 80);
}
if (isMotor) fetchMotorTestStatus();
}
@@ -515,6 +665,487 @@
});
}
function formatCoordinate(value) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return formatNumber(value, 5);
}
function formatAltitude(value) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return formatNumber(value, 1) + ' m';
}
function formatAccuracy(value) {
if (typeof value !== 'number' || isNaN(value) || value <= 0) return '--';
return formatNumber(value, 1) + ' m';
}
function formatFixTime(date) {
if (!date) return '--';
return date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function updateMapTelemetryPanel() {
setText('map_gps_status', roverMapState.gpsStatusText || 'Offline');
setText('map_gps_satellites', roverMapState.satellites);
setText('map_gps_lat', formatCoordinate(roverMapState.roverLat));
setText('map_gps_lon', formatCoordinate(roverMapState.roverLon));
setText('map_gps_alt', formatAltitude(roverMapState.roverAlt));
setText('map_gps_accuracy', formatAccuracy(roverMapState.accuracyMeters));
setText('map_gps_last_fix', formatFixTime(roverMapState.lastFixAt));
setText('map_route_mode', roverMapState.waypointPlanning ? 'aktiv' : 'aus');
setText('map_waypoint_count', roverMapState.waypoints.length);
}
function renderRouteLibrary() {
var select = document.getElementById('map_route_select');
if (!select) return;
var current = select.value;
var options = ['<option value="">Route wählen</option>'];
routeLibrary.forEach(function(route) {
options.push('<option value="' + route.name.replace(/"/g, '&quot;') + '">' + route.name + '</option>');
});
select.innerHTML = options.join('');
var stillExists = routeLibrary.some(function(route) {
return route.name === current;
});
select.value = stillExists ? current : '';
}
function applyWaypointPlanningState() {
var button = document.getElementById('map_plan_toggle');
if (button) {
button.textContent = 'Route planen: ' + (roverMapState.waypointPlanning ? 'an' : 'aus');
button.classList.toggle('is_active', roverMapState.waypointPlanning);
}
var mapNode = document.getElementById('rover_map');
if (mapNode) {
mapNode.classList.toggle('is_planning', roverMapState.waypointPlanning);
}
updateMapTelemetryPanel();
}
function renderWaypointList() {
var list = document.getElementById('map_waypoint_list');
if (!list) return;
if (!roverMapState.waypoints.length) {
list.innerHTML = '<div class="map-waypoint-empty">Noch keine Wegpunkte gesetzt.</div>';
updateMapTelemetryPanel();
return;
}
var html = roverMapState.waypoints.map(function(point, index) {
return (
'<div class="map-waypoint-item">' +
'<div class="map-waypoint-index">WP ' + (index + 1) + '</div>' +
'<div class="map-waypoint-coords">' +
'<strong>' + formatCoordinate(point.lat) + '</strong>' +
'<span>' + formatCoordinate(point.lon) + '</span>' +
'</div>' +
'</div>'
);
});
list.innerHTML = html.join('');
updateMapTelemetryPanel();
}
function createWaypointIcon(index) {
return L.divIcon({
className: 'map-waypoint-icon',
html:
'<div class="map-waypoint-stack">' +
'<div class="map-waypoint-label">WP ' + (index + 1) + '</div>' +
'<div class="map-waypoint-dot"></div>' +
'</div>',
iconSize: [58, 42],
iconAnchor: [29, 34]
});
}
function buildRouteCoordinates() {
var coords = [];
if (roverMapState.hasFix && typeof roverMapState.roverLat === 'number' && typeof roverMapState.roverLon === 'number') {
coords.push([roverMapState.roverLat, roverMapState.roverLon]);
}
roverMapState.waypoints.forEach(function(point) {
coords.push([point.lat, point.lon]);
});
return coords;
}
function renderPlannedRoute() {
if (!roverMapState.mapReady || !roverMapState.routeLine) return;
roverMapState.routeLine.setLatLngs(buildRouteCoordinates());
renderWaypointList();
}
function getWaypointInsertIndexFromRouteClick(latlng) {
if (!roverMapState.waypoints.length) return 0;
var clickLat = Number(latlng.lat);
var clickLon = Number(latlng.lng);
var bestIndex = roverMapState.waypoints.length;
var bestDistance = Number.POSITIVE_INFINITY;
for (var i = 0; i < roverMapState.waypoints.length - 1; i += 1) {
var start = roverMapState.waypoints[i];
var end = roverMapState.waypoints[i + 1];
var ax = Number(start.lon);
var ay = Number(start.lat);
var bx = Number(end.lon);
var by = Number(end.lat);
var px = clickLon;
var py = clickLat;
var abx = bx - ax;
var aby = by - ay;
var abLen2 = abx * abx + aby * aby;
var t = 0;
if (abLen2 > 0) {
t = ((px - ax) * abx + (py - ay) * aby) / abLen2;
t = Math.max(0, Math.min(1, t));
}
var projX = ax + abx * t;
var projY = ay + aby * t;
var dx = px - projX;
var dy = py - projY;
var distance = dx * dx + dy * dy;
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = i + 1;
}
}
return bestIndex;
}
function clearWaypointMarkers() {
roverMapState.waypointMarkers.forEach(function(marker) {
roverMapState.map.removeLayer(marker);
});
roverMapState.waypointMarkers = [];
}
function buildWaypointPopupContent(index, lat, lon) {
return (
'<div class="map-waypoint-popup">' +
'<div class="map-waypoint-popup-title">Wegpunkt ' + (index + 1) + '</div>' +
'<div class="map-waypoint-popup-coords">' + formatCoordinate(lat) + ', ' + formatCoordinate(lon) + '</div>' +
'<button class="map-waypoint-delete" onclick="removeWaypoint(' + index + ')">Wegpunkt löschen</button>' +
'</div>'
);
}
function renderWaypointMarkers() {
if (!roverMapState.mapReady) return;
clearWaypointMarkers();
roverMapState.waypoints.forEach(function(point, index) {
var marker = L.marker([point.lat, point.lon], {
title: 'Wegpunkt ' + (index + 1),
icon: createWaypointIcon(index),
draggable: true
}).addTo(roverMapState.map);
marker.bindPopup(buildWaypointPopupContent(index, point.lat, point.lon));
marker.on('drag', function(event) {
var latlng = event.target.getLatLng();
roverMapState.waypoints[index].lat = Number(latlng.lat);
roverMapState.waypoints[index].lon = Number(latlng.lng);
renderPlannedRoute();
});
marker.on('dragend', function(event) {
var latlng = event.target.getLatLng();
roverMapState.waypoints[index].lat = Number(latlng.lat);
roverMapState.waypoints[index].lon = Number(latlng.lng);
event.target.setPopupContent(buildWaypointPopupContent(index, latlng.lat, latlng.lng));
renderWaypointList();
renderPlannedRoute();
});
roverMapState.waypointMarkers.push(marker);
});
renderPlannedRoute();
}
function addWaypointFromMap(latlng) {
roverMapState.waypoints.push({
lat: Number(latlng.lat),
lon: Number(latlng.lng)
});
renderWaypointMarkers();
}
function insertWaypointAtIndex(index, latlng) {
var insertIndex = Math.max(0, Math.min(roverMapState.waypoints.length, Number(index)));
roverMapState.waypoints.splice(insertIndex, 0, {
lat: Number(latlng.lat),
lon: Number(latlng.lng)
});
renderWaypointMarkers();
}
function addWaypointAtMapCenter() {
if (!roverMapState.mapReady || !roverMapState.map) return;
addWaypointFromMap(roverMapState.map.getCenter());
}
function removeWaypoint(index) {
if (index < 0 || index >= roverMapState.waypoints.length) return;
roverMapState.waypoints.splice(index, 1);
renderWaypointMarkers();
}
async function refreshRouteLibrary() {
try {
var response = await fetch(adminApiBase + '/api/routes');
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
routeLibrary = Array.isArray(data.library && data.library.routes) ? data.library.routes : [];
renderRouteLibrary();
} catch (error) {
setAdminStatus(error.message || 'Routen konnten nicht geladen werden');
}
}
function loadSelectedRoute() {
var select = document.getElementById('map_route_select');
if (!select || !select.value) return;
var route = routeLibrary.find(function(item) {
return item.name === select.value;
});
if (!route) return;
roverMapState.waypoints = (route.waypoints || []).map(function(point) {
return {
lat: Number(point.lat),
lon: Number(point.lon)
};
});
var nameInput = document.getElementById('map_route_name');
if (nameInput) nameInput.value = route.name;
renderWaypointMarkers();
setAdminStatus('Route geladen');
}
async function saveCurrentRoute() {
var nameInput = document.getElementById('map_route_name');
var routeName = nameInput ? String(nameInput.value || '').trim() : '';
if (!routeName) {
alert('Bitte einen Routennamen eingeben.');
return;
}
if (!roverMapState.waypoints.length) {
alert('Die Route hat noch keine Wegpunkte.');
return;
}
try {
var response = await fetch(adminApiBase + '/api/routes/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: routeName,
waypoints: roverMapState.waypoints
})
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
routeLibrary = Array.isArray(data.library && data.library.routes) ? data.library.routes : routeLibrary;
renderRouteLibrary();
var select = document.getElementById('map_route_select');
if (select) select.value = routeName;
setAdminStatus(data.status || 'Route gespeichert');
} catch (error) {
alert(error.message || 'Die Route konnte nicht gespeichert werden.');
}
}
async function deleteSelectedRoute() {
var select = document.getElementById('map_route_select');
if (!select || !select.value) {
alert('Bitte zuerst eine gespeicherte Route auswählen.');
return;
}
if (!window.confirm('Diese gespeicherte Route wirklich löschen?')) return;
try {
var response = await fetch(adminApiBase + '/api/routes/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: select.value})
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
routeLibrary = Array.isArray(data.library && data.library.routes) ? data.library.routes : [];
renderRouteLibrary();
setAdminStatus(data.status || 'Route gelöscht');
} catch (error) {
alert(error.message || 'Die Route konnte nicht gelöscht werden.');
}
}
function toggleWaypointPlanning() {
roverMapState.waypointPlanning = !roverMapState.waypointPlanning;
applyWaypointPlanningState();
}
function clearPlannedRoute() {
roverMapState.waypoints = [];
renderWaypointMarkers();
}
function centerMapOnRover() {
if (!roverMapState.mapReady || !roverMapState.hasFix) return;
roverMapState.map.setView([roverMapState.roverLat, roverMapState.roverLon], Math.max(roverMapState.map.getZoom(), 17));
}
function initRoverMap() {
if (roverMapState.mapReady) return;
var fallback = document.getElementById('rover_map_fallback');
var mapNode = document.getElementById('rover_map');
if (!mapNode) return;
if (typeof L === 'undefined') {
return;
}
roverMapState.map = L.map('rover_map', {
zoomControl: true,
attributionControl: true,
maxZoom: 22
}).setView([50.2984, 11.0480], 16);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22,
maxNativeZoom: 19,
attribution: '&copy; OpenStreetMap'
}).addTo(roverMapState.map);
roverMapState.roverTrackLine = L.polyline([], {
color: '#00ff91',
weight: 3,
opacity: 0.7,
interactive: false
}).addTo(roverMapState.map);
roverMapState.routeLine = L.polyline([], {
color: '#0088FF',
weight: 3,
opacity: 0.85,
interactive: true
}).addTo(roverMapState.map);
roverMapState.routeLine.on('click', function(event) {
roverMapState.suppressMapClickUntil = Date.now() + 250;
if (event.originalEvent) {
if (typeof event.originalEvent.preventDefault === 'function') event.originalEvent.preventDefault();
if (typeof event.originalEvent.stopPropagation === 'function') event.originalEvent.stopPropagation();
}
if (!roverMapState.waypointPlanning) return;
if (roverMapState.waypoints.length < 2) {
addWaypointFromMap(event.latlng);
return;
}
insertWaypointAtIndex(getWaypointInsertIndexFromRouteClick(event.latlng), event.latlng);
});
roverMapState.roverAccuracyCircle = L.circle([50.2984, 11.0480], {
radius: 0,
color: '#00ff91',
weight: 1,
opacity: 0.7,
fillColor: '#00ff91',
fillOpacity: 0.1,
interactive: false
}).addTo(roverMapState.map);
roverMapState.roverMarker = L.circleMarker([50.2984, 11.0480], {
radius: 7,
color: '#05111d',
weight: 2,
fillColor: '#FF8C00',
fillOpacity: 1,
interactive: false
}).addTo(roverMapState.map);
roverMapState.map.on('click', function(event) {
if (Date.now() < roverMapState.suppressMapClickUntil) return;
if (!roverMapState.waypointPlanning) return;
addWaypointFromMap(event.latlng);
});
roverMapState.mapReady = true;
applyWaypointPlanningState();
renderWaypointMarkers();
updateMapTelemetryPanel();
}
function updateRoverPositionOnMap() {
if (!roverMapState.mapReady || !roverMapState.hasFix) return;
var latlng = [roverMapState.roverLat, roverMapState.roverLon];
roverMapState.roverMarker.setLatLng(latlng);
roverMapState.roverMarker.bindPopup(
'Rover<br>' +
formatCoordinate(roverMapState.roverLat) + ', ' + formatCoordinate(roverMapState.roverLon)
);
var accuracy = roverMapState.accuracyMeters || 0;
roverMapState.roverAccuracyCircle.setLatLng(latlng);
roverMapState.roverAccuracyCircle.setRadius(accuracy);
roverMapState.trackPoints.push(latlng);
if (roverMapState.trackPoints.length > 60) roverMapState.trackPoints.shift();
roverMapState.roverTrackLine.setLatLngs(roverMapState.trackPoints);
if (!roverMapState.hasCenteredOnFix) {
roverMapState.map.setView(latlng, 17);
roverMapState.hasCenteredOnFix = true;
}
renderPlannedRoute();
}
function updateGpsStatusSummary(text) {
roverMapState.gpsStatusText = text || 'Offline';
var satMatch = String(roverMapState.gpsStatusText).match(/(\d+)\s*Sat/i);
roverMapState.satellites = satMatch ? satMatch[1] : '--';
updateMapTelemetryPanel();
}
function handleGpsFix(message) {
if (!message || !message.status) return;
roverMapState.hasFix = Number(message.status.status) >= 0;
if (!roverMapState.hasFix) {
roverMapState.roverLat = null;
roverMapState.roverLon = null;
roverMapState.roverAlt = null;
roverMapState.accuracyMeters = null;
updateMapTelemetryPanel();
renderPlannedRoute();
return;
}
roverMapState.roverLat = Number(message.latitude);
roverMapState.roverLon = Number(message.longitude);
roverMapState.roverAlt = Number(message.altitude);
roverMapState.lastFixAt = new Date();
var covariance = Array.isArray(message.position_covariance) ? Number(message.position_covariance[0]) : 0;
roverMapState.accuracyMeters = covariance > 0 ? Math.sqrt(covariance) : null;
updateMapTelemetryPanel();
initRoverMap();
updateRoverPositionOnMap();
}
function formatSpeedCms(speedMps, digits) {
return formatNumber(Number(speedMps || 0) * 100, digits);
}
@@ -974,6 +1605,22 @@
driveEstimator.motorSpeeds = Array.isArray(message.motor_speeds) ? message.motor_speeds.map(Number) : [0, 0, 0, 0, 0, 0];
renderDriveEstimator();
});
gpsFixTopic = new ROSLIB.Topic({
ros: ros,
name: '/fix',
messageType: 'sensor_msgs/NavSatFix'
});
gpsFixTopic.subscribe(handleGpsFix);
gpsStatusTopic = new ROSLIB.Topic({
ros: ros,
name: '/gps/status_text',
messageType: 'std_msgs/String'
});
gpsStatusTopic.subscribe(function(message) {
updateGpsStatusSummary(message.data);
});
}
function temperatureToWidth(value) {
@@ -1477,6 +2124,8 @@
switchAdminSection('motor');
} else if (window.location.hash === '#drive') {
switchAdminSection('drive');
} else if (window.location.hash === '#map') {
switchAdminSection('map');
}
updateClock();
window.setInterval(updateClock, 1000);
@@ -1486,7 +2135,10 @@
fetchMotorTestStatus();
fetchDriveEstimatorConfig();
loadDriveEstimatorCalibration();
refreshRouteLibrary();
connectDriveEstimatorRos();
renderWaypointList();
applyWaypointPlanningState();
renderDriveEstimator();
window.setInterval(fetchStatus, 10000);
window.setInterval(fetchDelay, 5000);
+222
View File
@@ -1073,6 +1073,12 @@ body.page-admin .act-btn.danger:hover{ background:rgba(255,51,51,.12); color:#FF
body.page-admin .act-btn.full{ grid-column:span 2; }
body.page-admin .act-btn.subtle{ border-color:rgba(0,255,145,.1); color:rgba(0,255,145,.3); }
body.page-admin .act-btn.subtle:hover{ color:rgba(0,255,145,.6); border-color:rgba(0,255,145,.35); }
body.page-admin .act-btn.is_active{
background:rgba(0,136,255,.16);
color:#55aaff;
border-color:rgba(0,136,255,.55);
box-shadow:0 0 14px rgba(0,136,255,.18);
}
/* ── LINK CARD ──────────────────────────────────── */
body.page-admin .link-card{ display:flex; align-items:center; gap:12px; padding:10px 12px; }
@@ -1177,6 +1183,217 @@ body.page-admin .drive-calibration-actions{
body.page-admin .drive-calibration-button{
width:100%;
}
body.page-admin .admin-section-grid_map{
grid-template-columns:minmax(0, 1.7fr) minmax(280px, .85fr);
}
body.page-admin .admin-col_map_main,
body.page-admin .admin-col_map_side{ min-width:0; }
body.page-admin .map-panel{
display:flex;
flex-direction:column;
min-height:620px;
}
body.page-admin .map-toolbar{
display:flex;
gap:8px;
padding:10px 12px 0;
flex-wrap:wrap;
}
body.page-admin .map-copy{ padding:10px 12px; }
body.page-admin .map-route-storage{
display:grid;
grid-template-columns:auto 1fr auto;
gap:8px;
align-items:center;
padding:0 12px 10px;
}
body.page-admin .map-route-storage_secondary{
grid-template-columns:auto 1fr auto auto;
}
body.page-admin .map-route-storage-label{
font-size:9px;
letter-spacing:1.4px;
color:rgba(210,255,235,.9);
text-transform:uppercase;
}
body.page-admin .map-route-storage-input{
font-family:var(--mono);
font-size:10px;
letter-spacing:1px;
color:rgba(245,255,250,.96);
background:#050A14;
border:1px solid rgba(120,255,205,.36);
padding:7px 10px;
outline:none;
min-width:0;
}
body.page-admin .map-route-storage-input:focus{
border-color:rgba(0,255,145,.5);
}
body.page-admin .map-stage{
position:relative;
flex:1;
min-height:460px;
margin:0 12px 12px;
border:1px solid rgba(0,255,145,.12);
background:
radial-gradient(circle at 20% 20%, rgba(0,136,255,.12), transparent 34%),
radial-gradient(circle at 78% 24%, rgba(0,255,145,.08), transparent 28%),
#04101a;
overflow:hidden;
}
body.page-admin .rover-map,
body.page-admin .rover-map-fallback{
position:absolute;
inset:0;
}
body.page-admin .rover-map.is_planning{
cursor: crosshair;
}
body.page-admin .rover-map-fallback{
display:flex;
align-items:center;
justify-content:center;
text-align:center;
padding:20px;
color:rgba(205,255,235,.86);
font-size:12px;
letter-spacing:.6px;
background:rgba(4,16,26,.92);
}
body.page-admin .map-hidden{
display:none !important;
}
body.page-admin .map-waypoint-list{
display:flex;
flex-direction:column;
gap:8px;
padding:10px 12px 12px;
}
body.page-admin .map-waypoint-empty{
padding:12px;
border:1px dashed rgba(0,255,145,.14);
color:rgba(180,255,225,.66);
font-size:8px;
letter-spacing:1px;
text-transform:uppercase;
text-align:center;
}
body.page-admin .map-waypoint-item{
display:flex;
align-items:center;
gap:10px;
padding:10px;
border:1px solid rgba(0,255,145,.1);
background:rgba(0,255,145,.03);
}
body.page-admin .map-waypoint-index{
min-width:42px;
font-size:8px;
color:rgba(180,255,225,.72);
letter-spacing:1.4px;
text-transform:uppercase;
}
body.page-admin .map-waypoint-coords{
display:flex;
flex-direction:column;
gap:3px;
min-width:0;
}
body.page-admin .map-waypoint-coords strong,
body.page-admin .map-waypoint-coords span{
font-size:10px;
letter-spacing:.5px;
color:rgba(245,255,250,.98);
}
body.page-admin .map-waypoint-icon{
background:transparent;
border:none;
}
body.page-admin .map-waypoint-stack{
display:flex;
flex-direction:column;
align-items:center;
gap:6px;
}
body.page-admin .map-waypoint-label{
display:flex;
align-items:center;
justify-content:center;
min-width:46px;
height:24px;
padding:0 8px;
border-radius:999px;
border:1px solid rgba(0,136,255,.55);
background:rgba(3,19,31,.94);
color:#dff7ff;
font-size:10px;
font-weight:700;
letter-spacing:.6px;
box-shadow:0 0 14px rgba(0,136,255,.16);
white-space:nowrap;
user-select:none;
}
body.page-admin .map-waypoint-dot{
width:14px;
height:14px;
border-radius:50%;
border:2px solid #03131f;
background:#0088FF;
box-shadow:0 0 14px rgba(0,136,255,.24);
cursor:grab;
user-select:none;
}
body.page-admin .map-waypoint-label:active,
body.page-admin .map-waypoint-dot:active{
cursor:grabbing;
}
body.page-admin .map-waypoint-popup{
display:flex;
flex-direction:column;
gap:8px;
min-width:160px;
}
body.page-admin .map-waypoint-popup-title{
font-size:11px;
font-weight:700;
color:#eefcff;
letter-spacing:.5px;
}
body.page-admin .map-waypoint-popup-coords{
font-size:10px;
color:rgba(205,255,235,.86);
letter-spacing:.4px;
}
body.page-admin .map-waypoint-delete{
font-family:var(--mono);
font-size:10px;
font-weight:700;
letter-spacing:.5px;
border:1px solid rgba(255,51,51,.45);
background:rgba(255,51,51,.08);
color:#ffd7d7;
padding:8px 10px;
cursor:pointer;
}
body.page-admin .map-waypoint-delete:hover{
background:rgba(255,51,51,.16);
color:#fff2f2;
}
body.page-admin .leaflet-container{
font-family:var(--mono);
background:#04101a;
}
body.page-admin .leaflet-control-attribution{
background:rgba(4,16,26,.82);
color:rgba(180,255,225,.72);
}
body.page-admin .leaflet-control-attribution a{ color:rgba(85,170,255,.9); }
body.page-admin .leaflet-popup-content-wrapper,
body.page-admin .leaflet-popup-tip{
background:#07111c;
color:rgba(235,255,245,.96);
}
@media (max-width:479px){
body.page-admin .drive-estimator-toolbar{ flex-direction:column; align-items:stretch; }
body.page-admin .drive-estimator-input{ grid-template-columns:1fr; }
@@ -1184,6 +1401,11 @@ body.page-admin .drive-calibration-button{
body.page-admin .drive-estimator-value{ font-size:28px; }
body.page-admin .drive-calibration-toolbar{ flex-direction:column; align-items:stretch; }
body.page-admin .drive-calibration-grid{ grid-template-columns:1fr; }
body.page-admin .admin-section-grid_map{ grid-template-columns:1fr; }
body.page-admin .map-panel{ min-height:0; }
body.page-admin .map-stage{ min-height:360px; }
body.page-admin .map-route-storage,
body.page-admin .map-route-storage_secondary{ grid-template-columns:1fr; }
}
/* ── AUTH OVERLAY ───────────────────────────────── */
+661
View File
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
File diff suppressed because one or more lines are too long
@@ -18,6 +18,9 @@ SPEED_LIMIT_FILE = '/tmp/exomy_speed_limit.txt'
DRIVE_ESTIMATOR_CALIBRATION_FILE = os.path.join(
os.path.dirname(__file__), '..', 'config', 'drive_estimator_calibration.json'
)
ROUTE_LIBRARY_FILE = os.path.join(
os.path.dirname(__file__), '..', 'config', 'route_library.json'
)
MOTOR_TEST_LOCK = threading.Lock()
@@ -625,6 +628,156 @@ def save_drive_estimator_calibration(points):
}
def get_route_library():
data = {
'routes': [],
'updated_at': None,
}
try:
with open(ROUTE_LIBRARY_FILE, 'r', encoding='utf-8') as handle:
payload = json.load(handle)
routes = payload.get('routes')
if isinstance(routes, list):
cleaned_routes = []
for route in routes:
if not isinstance(route, dict):
continue
name = str(route.get('name', '')).strip()
waypoints = route.get('waypoints', [])
if not name or not isinstance(waypoints, list):
continue
cleaned_waypoints = []
for point in waypoints:
if not isinstance(point, dict):
continue
try:
lat = float(point.get('lat'))
lon = float(point.get('lon'))
except (TypeError, ValueError):
continue
cleaned_waypoints.append({
'lat': lat,
'lon': lon,
})
cleaned_routes.append({
'name': name,
'waypoints': cleaned_waypoints,
'updated_at': route.get('updated_at'),
})
data = {
'routes': cleaned_routes,
'updated_at': payload.get('updated_at'),
}
except FileNotFoundError:
pass
except (OSError, ValueError, TypeError, json.JSONDecodeError):
return 500, {'status': 'Routen konnten nicht geladen werden'}
return 200, {
'status': 'Bereit',
'library': data,
}
def save_named_route(name, waypoints):
route_name = str(name or '').strip()
if not route_name:
return 400, {'status': 'Routenname fehlt'}
if not isinstance(waypoints, list) or not waypoints:
return 400, {'status': 'Route hat keine Wegpunkte'}
cleaned_waypoints = []
for point in waypoints:
if not isinstance(point, dict):
return 400, {'status': 'Wegpunkte sind ungültig'}
try:
lat = float(point.get('lat'))
lon = float(point.get('lon'))
except (TypeError, ValueError):
return 400, {'status': 'Wegpunkte sind ungültig'}
cleaned_waypoints.append({
'lat': lat,
'lon': lon,
})
existing_status, existing_response = get_route_library()
if existing_status != 200:
return existing_status, existing_response
routes = existing_response['library']['routes']
now_text = time.strftime('%Y-%m-%d %H:%M:%S')
replaced = False
for route in routes:
if route['name'].lower() == route_name.lower():
route['name'] = route_name
route['waypoints'] = cleaned_waypoints
route['updated_at'] = now_text
replaced = True
break
if not replaced:
routes.append({
'name': route_name,
'waypoints': cleaned_waypoints,
'updated_at': now_text,
})
routes.sort(key=lambda item: item['name'].lower())
os.makedirs(os.path.dirname(ROUTE_LIBRARY_FILE), exist_ok=True)
payload = {
'updated_at': now_text,
'routes': routes,
}
try:
with open(ROUTE_LIBRARY_FILE, 'w', encoding='utf-8') as handle:
json.dump(payload, handle, ensure_ascii=False, indent=2)
handle.write('\n')
except OSError:
return 500, {'status': 'Route konnte nicht gespeichert werden'}
return 200, {
'status': 'Route gespeichert',
'route': {
'name': route_name,
'waypoints': cleaned_waypoints,
'updated_at': now_text,
},
'library': payload,
}
def delete_named_route(name):
route_name = str(name or '').strip()
if not route_name:
return 400, {'status': 'Routenname fehlt'}
existing_status, existing_response = get_route_library()
if existing_status != 200:
return existing_status, existing_response
routes = existing_response['library']['routes']
filtered_routes = [route for route in routes if route['name'].lower() != route_name.lower()]
if len(filtered_routes) == len(routes):
return 404, {'status': 'Route nicht gefunden'}
now_text = time.strftime('%Y-%m-%d %H:%M:%S')
payload = {
'updated_at': now_text,
'routes': filtered_routes,
}
try:
with open(ROUTE_LIBRARY_FILE, 'w', encoding='utf-8') as handle:
json.dump(payload, handle, ensure_ascii=False, indent=2)
handle.write('\n')
except OSError:
return 500, {'status': 'Route konnte nicht gelöscht werden'}
return 200, {
'status': 'Route gelöscht',
'library': payload,
}
def preview_drive_neutral_action(values):
from admin_motor_test import preview_drive_neutral_value
@@ -728,9 +881,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
status_code, response = get_drive_estimator_calibration()
self.write_json(status_code, response)
return
if self.path == '/api/routes':
status_code, response = get_route_library()
self.write_json(status_code, response)
return
self.write_json(404, {'status': 'Unbekannt'})
def do_POST(self):
raw_body = b'{}'
if self.path in ('/api/routes/save', '/api/routes/delete'):
try:
content_length = int(self.headers.get('Content-Length', '0'))
except ValueError:
content_length = 0
raw_body = self.rfile.read(content_length) if content_length > 0 else b'{}'
if self.path == '/api/delay':
try:
content_length = int(self.headers.get('Content-Length', '0'))
@@ -782,6 +947,31 @@ class Handler(http.server.BaseHTTPRequestHandler):
self.write_json(status_code, response)
return
if self.path == '/api/routes/save':
try:
payload = json.loads(raw_body.decode('utf-8'))
except (UnicodeDecodeError, json.JSONDecodeError):
self.write_json(400, {'status': 'Ungültige Anfrage'})
return
status_code, response = save_named_route(
payload.get('name'),
payload.get('waypoints'),
)
self.write_json(status_code, response)
return
if self.path == '/api/routes/delete':
try:
payload = json.loads(raw_body.decode('utf-8'))
except (UnicodeDecodeError, json.JSONDecodeError):
self.write_json(400, {'status': 'Ungültige Anfrage'})
return
status_code, response = delete_named_route(payload.get('name'))
self.write_json(status_code, response)
return
if self.path == '/api/motor-test/drive-neutral/preview':
try:
content_length = int(self.headers.get('Content-Length', '0'))