Screenshot rein und Uhrzeit aus GPS
This commit is contained in:
+214
-87
@@ -35,6 +35,13 @@
|
||||
<div class="hdr-div"></div>
|
||||
<div class="hdr-badge">Mission Ops</div>
|
||||
<div class="hdr-gap"></div>
|
||||
<button class="header-icon-btn" id="save-frame-button" type="button" aria-label="Screenshot speichern" title="Screenshot speichern">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M7 4h7l2 2h3a2 2 0 0 1 2 2v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V8a2 2 0 0 1 2-2h2V4z"></path>
|
||||
<path d="M12 9.2a4.3 4.3 0 1 0 0 8.6a4.3 4.3 0 0 0 0-8.6z"></path>
|
||||
<circle cx="18" cy="9" r="1.2"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="admin-link" href="./admin.html">Admin</a>
|
||||
<div class="hdr-div"></div>
|
||||
<div class="mission-ok"><div class="pulse-dot"></div><span id="cuno-connection-status">Cuno offline</span></div>
|
||||
@@ -86,11 +93,6 @@
|
||||
V: <span id="overlay-speed">0,0</span> cm/s<br>
|
||||
S: <span id="overlay-distance">0,00</span> m
|
||||
</div>
|
||||
<svg class="c-track" id="track-overlay" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path id="track-center" class="track-center" d=""></path>
|
||||
<path id="track-left" class="track-edge" d=""></path>
|
||||
<path id="track-right" class="track-edge" d=""></path>
|
||||
</svg>
|
||||
<div class="c-xhair">
|
||||
<svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
|
||||
<!-- äußerer rotierender Strichelring -->
|
||||
@@ -315,10 +317,6 @@ var driveEstimator = {
|
||||
motorSpeeds: [0, 0, 0, 0, 0, 0],
|
||||
calibrationPoints: []
|
||||
};
|
||||
var trajectoryOverlay = {
|
||||
steering: 90,
|
||||
velocity: 0
|
||||
};
|
||||
var pageConnection = {
|
||||
apiOk: false,
|
||||
apiLastAt: 0,
|
||||
@@ -477,78 +475,6 @@ function setPath(id, d) {
|
||||
if (node) node.setAttribute("d", d || "");
|
||||
}
|
||||
|
||||
function renderTrajectoryOverlay() {
|
||||
var overlay = document.getElementById("track-overlay");
|
||||
if (!overlay) return;
|
||||
|
||||
var moving = driveEstimator.motorsEnabled && Math.abs(driveEstimator.speedMps) > 0.005;
|
||||
overlay.style.opacity = moving ? "1" : "0.22";
|
||||
var baseX = 50;
|
||||
var baseY = 92;
|
||||
var laneHalf = 9;
|
||||
var topY = 18;
|
||||
var leftStart = baseX - laneHalf;
|
||||
var rightStart = baseX + laneHalf;
|
||||
|
||||
if (!moving) {
|
||||
setPath("track-center", "M " + baseX + " " + baseY + " L " + baseX + " " + topY);
|
||||
setPath("track-left", "M " + leftStart + " " + baseY + " L " + leftStart + " " + topY);
|
||||
setPath("track-right", "M " + rightStart + " " + baseY + " L " + rightStart + " " + topY);
|
||||
return;
|
||||
}
|
||||
|
||||
var mode = driveEstimator.locomotionMode;
|
||||
var steering = Number(trajectoryOverlay.steering || 90);
|
||||
var direction = driveEstimator.speedMps < -0.005 ? -1 : 1;
|
||||
|
||||
if (mode === 3) {
|
||||
var crabOffset = Math.max(-24, Math.min(24, (steering >= 0 ? steering - 90 : steering + 90) * 0.26));
|
||||
setPath("track-center", "M " + baseX + " " + baseY + " L " + (baseX + crabOffset) + " " + topY);
|
||||
setPath("track-left", "M " + (baseX - laneHalf) + " " + baseY + " L " + (baseX - laneHalf + crabOffset) + " " + topY);
|
||||
setPath("track-right", "M " + (baseX + laneHalf) + " " + baseY + " L " + (baseX + laneHalf + crabOffset) + " " + topY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 2) {
|
||||
var pointTurnRight = steering > 95 || steering < -95;
|
||||
var sweep = pointTurnRight ? 1 : 0;
|
||||
var radiusOuter = 34;
|
||||
var radiusInner = 20;
|
||||
var centerX = pointTurnRight ? 78 : 22;
|
||||
var startLeftX = pointTurnRight ? 40 : 24;
|
||||
var startRightX = pointTurnRight ? 58 : 42;
|
||||
setPath("track-center", "M " + baseX + " " + baseY + " A 27 27 0 0 " + sweep + " " + centerX + " 42");
|
||||
setPath("track-left", "M " + startLeftX + " " + baseY + " A " + radiusOuter + " " + radiusOuter + " 0 0 " + sweep + " " + centerX + " 18");
|
||||
setPath("track-right", "M " + startRightX + " " + baseY + " A " + radiusInner + " " + radiusInner + " 0 0 " + sweep + " " + centerX + " 34");
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedTurn;
|
||||
if (steering > 90) {
|
||||
normalizedTurn = Math.min(1, (steering - 90) / 90);
|
||||
} else {
|
||||
normalizedTurn = -Math.min(1, (90 - steering) / 90);
|
||||
}
|
||||
normalizedTurn *= direction;
|
||||
|
||||
var endX = baseX + normalizedTurn * 26;
|
||||
var controlY = 58;
|
||||
var controlBias = normalizedTurn * 18;
|
||||
var centerPath = "M " + baseX + " " + baseY + " Q " + (baseX + controlBias * 0.6) + " " + controlY + " " + endX + " " + topY;
|
||||
var leftPath = "M " + leftStart + " " + baseY + " Q " + (leftStart + controlBias) + " " + controlY + " " + (endX - laneHalf) + " " + topY;
|
||||
var rightPath = "M " + rightStart + " " + baseY + " Q " + (rightStart + controlBias) + " " + controlY + " " + (endX + laneHalf) + " " + topY;
|
||||
|
||||
if (Math.abs(normalizedTurn) < 0.04) {
|
||||
centerPath = "M " + baseX + " " + baseY + " L " + baseX + " " + topY;
|
||||
leftPath = "M " + leftStart + " " + baseY + " L " + leftStart + " " + topY;
|
||||
rightPath = "M " + rightStart + " " + baseY + " L " + rightStart + " " + topY;
|
||||
}
|
||||
|
||||
setPath("track-center", centerPath);
|
||||
setPath("track-left", leftPath);
|
||||
setPath("track-right", rightPath);
|
||||
}
|
||||
|
||||
function fetchDriveEstimatorData() {
|
||||
fetch(window.location.protocol + "//" + hostUrl + ":8082/api/drive-estimator/config", {
|
||||
cache: "no-store"
|
||||
@@ -1096,6 +1022,211 @@ function selectDriveMode(mode, buttonIndex) {
|
||||
pulseButton(buttonIndex);
|
||||
}
|
||||
|
||||
function loadImageFromBlob(blob) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var image = new Image();
|
||||
var objectUrl = URL.createObjectURL(blob);
|
||||
image.onload = function () {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = function () {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
reject(new Error("image"));
|
||||
};
|
||||
image.src = objectUrl;
|
||||
});
|
||||
}
|
||||
|
||||
function drawOverlayTextBlock(context, lines, x, y, align, lineHeight, color) {
|
||||
context.save();
|
||||
context.textAlign = align || "left";
|
||||
context.textBaseline = "top";
|
||||
context.fillStyle = color;
|
||||
for (var i = 0; i < lines.length; i += 1) {
|
||||
context.fillText(lines[i], x, y + (i * lineHeight));
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawCrosshairOverlayToCanvas(context, canvasWidth, canvasHeight, size) {
|
||||
var cx = canvasWidth / 2;
|
||||
var cy = canvasHeight / 2;
|
||||
var unit = size / 180;
|
||||
|
||||
function circle(radius, stroke, lineWidth, dash) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.arc(cx, cy, radius * unit, 0, Math.PI * 2);
|
||||
if (dash) context.setLineDash(dash.map(function (value) { return value * unit; }));
|
||||
context.strokeStyle = stroke;
|
||||
context.lineWidth = lineWidth * unit;
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function line(x1, y1, x2, y2, stroke, lineWidth) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.moveTo(cx + (x1 * unit), cy + (y1 * unit));
|
||||
context.lineTo(cx + (x2 * unit), cy + (y2 * unit));
|
||||
context.strokeStyle = stroke;
|
||||
context.lineWidth = lineWidth * unit;
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function path(points, stroke, lineWidth) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.moveTo(cx + (points[0][0] * unit), cy + (points[0][1] * unit));
|
||||
for (var i = 1; i < points.length; i += 1) {
|
||||
context.lineTo(cx + (points[i][0] * unit), cy + (points[i][1] * unit));
|
||||
}
|
||||
context.strokeStyle = stroke;
|
||||
context.lineWidth = lineWidth * unit;
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
circle(82, "rgba(0,255,145,.2)", 1, [4, 10]);
|
||||
circle(60, "rgba(0,255,145,.15)", 0.75, [2, 16]);
|
||||
circle(16, "rgba(0,255,145,.3)", 0.75);
|
||||
line(-86, 0, -19, 0, "rgba(0,255,145,.6)", 1);
|
||||
line(19, 0, 86, 0, "rgba(0,255,145,.6)", 1);
|
||||
line(0, -86, 0, -19, "rgba(0,255,145,.6)", 1);
|
||||
line(0, 19, 0, 86, "rgba(0,255,145,.6)", 1);
|
||||
line(-60, -6, -60, 6, "rgba(0,255,145,.45)", 0.75);
|
||||
line(60, -6, 60, 6, "rgba(0,255,145,.45)", 0.75);
|
||||
line(-6, -60, 6, -60, "rgba(0,255,145,.45)", 0.75);
|
||||
line(-6, 60, 6, 60, "rgba(0,255,145,.45)", 0.75);
|
||||
line(-38, -3.5, -38, 3.5, "rgba(0,255,145,.3)", 0.5);
|
||||
line(38, -3.5, 38, 3.5, "rgba(0,255,145,.3)", 0.5);
|
||||
line(-3.5, -38, 3.5, -38, "rgba(0,255,145,.3)", 0.5);
|
||||
line(-3.5, 38, 3.5, 38, "rgba(0,255,145,.3)", 0.5);
|
||||
path([[-74, -58], [-74, -74], [-58, -74]], "rgba(0,255,145,.75)", 1.5);
|
||||
path([[58, -74], [74, -74], [74, -58]], "rgba(0,255,145,.75)", 1.5);
|
||||
path([[-74, 58], [-74, 74], [-58, 74]], "rgba(0,255,145,.75)", 1.5);
|
||||
path([[58, 74], [74, 74], [74, 58]], "rgba(0,255,145,.75)", 1.5);
|
||||
circle(6, "rgba(0,255,145,.7)", 1);
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.arc(cx, cy, 2.5 * unit, 0, Math.PI * 2);
|
||||
context.fillStyle = "rgba(0,255,145,.9)";
|
||||
context.fill();
|
||||
context.beginPath();
|
||||
context.arc(cx, cy, 1 * unit, 0, Math.PI * 2);
|
||||
context.fillStyle = "rgba(255,255,255,.7)";
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawVignetteAndScanlines(context, canvasWidth, canvasHeight) {
|
||||
context.save();
|
||||
for (var y = 0; y < canvasHeight; y += 4) {
|
||||
context.fillStyle = "rgba(0,255,145,.012)";
|
||||
context.fillRect(0, y, canvasWidth, 1);
|
||||
}
|
||||
var vignette = context.createRadialGradient(
|
||||
canvasWidth * 0.5, canvasHeight * 0.5, canvasWidth * 0.25,
|
||||
canvasWidth * 0.5, canvasHeight * 0.5, canvasWidth * 0.75
|
||||
);
|
||||
vignette.addColorStop(0, "rgba(0,0,0,0)");
|
||||
vignette.addColorStop(1, "rgba(0,0,0,.58)");
|
||||
context.fillStyle = vignette;
|
||||
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawHudOverlayToCanvas(context, canvasWidth, canvasHeight) {
|
||||
var frame = document.getElementById("video_frame");
|
||||
var scaleValue = frame ? parseFloat(getComputedStyle(frame).getPropertyValue("--cam-scale")) : 1;
|
||||
var camScale = isFinite(scaleValue) && scaleValue > 0 ? scaleValue : 1;
|
||||
var frameWidth = frame ? frame.clientWidth : canvasWidth;
|
||||
var scaleToCanvas = frameWidth ? (canvasWidth / frameWidth) : 1;
|
||||
var padding = 13 * camScale * scaleToCanvas;
|
||||
var fontSize = 9 * camScale * scaleToCanvas;
|
||||
var lineHeight = fontSize * 1.9;
|
||||
var hudColor = "rgba(0,255,145,.72)";
|
||||
|
||||
context.save();
|
||||
context.font = fontSize + 'px "IBM Plex Mono", "Roboto Mono", monospace';
|
||||
context.fillStyle = hudColor;
|
||||
|
||||
drawOverlayTextBlock(context, [
|
||||
"CAM: " + document.getElementById("cam-state").textContent,
|
||||
"MODE: " + document.getElementById("overlay-mode").textContent
|
||||
], padding, padding, "left", lineHeight, hudColor);
|
||||
|
||||
drawOverlayTextBlock(context, [
|
||||
"ROS: " + document.getElementById("ros-state").textContent,
|
||||
"CTRL: " + document.getElementById("input-state").textContent
|
||||
], canvasWidth - padding, padding, "right", lineHeight, hudColor);
|
||||
|
||||
drawOverlayTextBlock(context, [
|
||||
"GPS: " + document.getElementById("gps-state").textContent,
|
||||
"POS: " + document.getElementById("gps-accuracy").textContent,
|
||||
"LAT: " + document.getElementById("gps-lat").textContent,
|
||||
"LON: " + document.getElementById("gps-lon").textContent
|
||||
], padding, canvasHeight - padding - (lineHeight * 4), "left", lineHeight, hudColor);
|
||||
|
||||
drawOverlayTextBlock(context, [
|
||||
"HOST: " + document.getElementById("cam-host").textContent,
|
||||
"STREAM: 8083"
|
||||
], canvasWidth - padding, canvasHeight - padding - (lineHeight * 2), "right", lineHeight, hudColor);
|
||||
|
||||
drawOverlayTextBlock(context, [
|
||||
"V: " + document.getElementById("overlay-speed").textContent + " cm/s",
|
||||
"S: " + document.getElementById("overlay-distance").textContent + " m"
|
||||
], canvasWidth / 2, canvasHeight - padding - (lineHeight * 2), "center", lineHeight, hudColor);
|
||||
|
||||
var recY = padding + (fontSize * 0.55);
|
||||
context.beginPath();
|
||||
context.arc((canvasWidth / 2) - (18 * camScale * scaleToCanvas), recY, 3 * camScale * scaleToCanvas, 0, Math.PI * 2);
|
||||
context.fillStyle = document.getElementById("cam-live-indicator").classList.contains("is-offline")
|
||||
? "rgba(170,255,220,.72)"
|
||||
: "#FF3333";
|
||||
context.fill();
|
||||
context.fillStyle = hudColor;
|
||||
context.textAlign = "center";
|
||||
context.fillText(document.getElementById("cam-live-text").textContent, canvasWidth / 2 + (6 * camScale * scaleToCanvas), padding);
|
||||
context.restore();
|
||||
|
||||
drawCrosshairOverlayToCanvas(context, canvasWidth, canvasHeight, 180 * camScale * scaleToCanvas);
|
||||
drawVignetteAndScanlines(context, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
async function saveCurrentFrame() {
|
||||
try {
|
||||
var response = await fetch("//" + hostUrl + ":8083/snapshot.jpg?t=" + Date.now(), {
|
||||
cache: "no-store"
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("snapshot");
|
||||
}
|
||||
var blob = await response.blob();
|
||||
var image = await loadImageFromBlob(blob);
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = image.naturalWidth || image.width;
|
||||
canvas.height = image.naturalHeight || image.height;
|
||||
var context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("canvas");
|
||||
}
|
||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
drawHudOverlayToCanvas(context, canvas.width, canvas.height);
|
||||
var link = document.createElement("a");
|
||||
var timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
var objectUrl = canvas.toDataURL("image/jpeg", 0.95);
|
||||
link.href = objectUrl;
|
||||
link.download = "exomy-" + timestamp + ".jpg";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function toggleMotors() {
|
||||
if (controlsAreBlocked()) {
|
||||
return;
|
||||
@@ -1281,12 +1412,10 @@ window.addEventListener("load", function () {
|
||||
setPageConnectionState();
|
||||
fetchDriveEstimatorData();
|
||||
renderDriveEstimatorOverlay();
|
||||
renderTrajectoryOverlay();
|
||||
statusTimer = window.setInterval(fetchSystemStatus, 5000);
|
||||
window.setInterval(refreshRosSocketState, 1000);
|
||||
window.setInterval(setPageConnectionState, 1000);
|
||||
window.setInterval(renderDriveEstimatorOverlay, 250);
|
||||
window.setInterval(renderTrajectoryOverlay, 250);
|
||||
|
||||
var video = document.getElementById("video_output");
|
||||
reloadCameraStream();
|
||||
@@ -1383,11 +1512,8 @@ window.addEventListener("load", function () {
|
||||
motorsEnabled = !!message.motors_enabled;
|
||||
driveEstimator.motorsEnabled = !!message.motors_enabled;
|
||||
driveEstimator.locomotionMode = Number(message.locomotion_mode);
|
||||
trajectoryOverlay.steering = Number(message.steering || 90);
|
||||
trajectoryOverlay.velocity = Number(message.vel || 0);
|
||||
updateMotorDisplay();
|
||||
renderDriveEstimatorOverlay();
|
||||
renderTrajectoryOverlay();
|
||||
});
|
||||
|
||||
motorCommandsListener = new ROSLIB.Topic({
|
||||
@@ -1401,7 +1527,6 @@ window.addEventListener("load", function () {
|
||||
setRosStatus("Verbunden");
|
||||
driveEstimator.motorSpeeds = Array.isArray(message.motor_speeds) ? message.motor_speeds.map(Number) : [0, 0, 0, 0, 0, 0];
|
||||
renderDriveEstimatorOverlay();
|
||||
renderTrajectoryOverlay();
|
||||
});
|
||||
|
||||
gpsFixListener = new ROSLIB.Topic({
|
||||
@@ -1449,6 +1574,8 @@ window.addEventListener("load", function () {
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("save-frame-button").addEventListener("click", saveCurrentFrame);
|
||||
|
||||
document.getElementById("motor-toggle").addEventListener("click", toggleMotors);
|
||||
|
||||
document.getElementById("mt-joy").addEventListener("click", function () {
|
||||
|
||||
Reference in New Issue
Block a user