Routenplanung eingebaut
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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, '"') + '">' + 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: '© 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);
|
||||
|
||||
@@ -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 ───────────────────────────────── */
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user