@@ -505,7 +507,9 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
const statusEl = document.getElementById('satpassStatus');
const canvas = document.getElementById('satSkyCanvas');
const exportBtn = document.getElementById('satpassExportBtn');
+ const exportSvgBtn = document.getElementById('satpassExportSvgBtn');
const invertBtn = document.getElementById('satpassInvertBtn');
+ const mirrorBtn = document.getElementById('satpassMirrorBtn');
const skyTimeEl = document.getElementById('satpassSkyTime');
const satSelectEl = document.getElementById('satSelect');
const passSelectEl = document.getElementById('passSelect');
@@ -513,7 +517,9 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
const ctx = canvas.getContext('2d');
const exportLogoImg = new Image();
exportLogoImg.src = 'images/logoAM_sw.png';
+ const EXPORT_BACKGROUND_COLOR = '#f3efe6';
let invertSky = false;
+ let mirrorEastWest = true;
let exportOpaqueSky = false;
let lastReferenceTime = null;
let lastShowStars = false;
@@ -521,6 +527,20 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
let lastSelectedPass = null;
let lastSkyData = [];
+ function getDisplayedAzimuthDeg(azimuthDeg) {
+ const normalizedAzimuth = ((Number(azimuthDeg) % 360) + 360) % 360;
+ if (!mirrorEastWest) return normalizedAzimuth;
+ return (360 - normalizedAzimuth) % 360;
+ }
+
+ function updateMirrorButton() {
+ if (!mirrorBtn) return;
+ mirrorBtn.textContent = mirrorEastWest
+ ? 'Ost links / West rechts'
+ : 'West links / Ost rechts';
+ mirrorBtn.classList.toggle('is-active', mirrorEastWest);
+ }
+
function deg2rad(deg) { return deg * Math.PI / 180; }
function rad2deg(rad) { return rad * 180 / Math.PI; }
@@ -594,13 +614,15 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
}
function azToCardinal(az) {
- const dirs = ['N','NO','O','SO','S','SW','W','NW'];
- return dirs[Math.round(az/45) % 8];
+ const displayAz = getDisplayedAzimuthDeg(az);
+ const dirs = ['N','NW','W','SW','S','SO','O','NO'];
+ return dirs[Math.round(displayAz/45) % 8];
}
function azToDirectionName(az) {
- const dirs = ['Norden','Nordosten','Osten','Suedosten','Sueden','Suedwesten','Westen','Nordwesten'];
- return dirs[Math.round(az/45) % 8];
+ const displayAz = getDisplayedAzimuthDeg(az);
+ const dirs = ['Norden','Nordwesten','Westen','Suedwesten','Sueden','Suedosten','Osten','Nordosten'];
+ return dirs[Math.round(displayAz/45) % 8];
}
function altitudeToPhrase(el) {
@@ -857,10 +879,84 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
function projectPoint(azimuthDeg, elevationDeg, radius, cx, cy) {
const r = ((90 - elevationDeg) / 90) * radius;
- const a = deg2rad(azimuthDeg);
+ const a = deg2rad(getDisplayedAzimuthDeg(azimuthDeg));
return { x: cx + r*Math.sin(a), y: cy - r*Math.cos(a) };
}
+ function getSkyRenderMetrics() {
+ const box = canvas.parentElement.getBoundingClientRect();
+ const size = Math.max(320, Math.floor(Math.min(box.width, 820)));
+ return { size, cx: size / 2, cy: size / 2, radius: size * 0.43 };
+ }
+
+ function getSkyPalette() {
+ return invertSky ? {
+ skyFill: exportOpaqueSky ? 'rgb(252,252,248)' : 'rgba(252,252,248,0.98)',
+ grid:'rgba(60,64,76,0.26)', gridText:'rgba(40,44,56,0.90)', cardinal:'#171b24',
+ constellationLine:'rgba(88,96,118,0.76)', constellationLabel:'rgba(44,50,66,0.92)',
+ star: a=>`rgba(10,12,18,${Math.max(0.72,a).toFixed(2)})`,
+ starLabel:'rgba(18,22,32,0.94)', shadowLine:'rgba(150,154,166,0.82)',
+ shadowMarker:'rgba(118,124,138,0.95)', shadowMarkerStroke:'rgba(24,28,36,0.95)',
+ shadowLabel:'#111722'
+ } : {
+ skyFill: exportOpaqueSky ? 'rgb(4,6,20)' : 'rgba(4,6,20,0.55)',
+ grid:'rgba(201,168,76,0.18)', gridText:'rgba(136,153,187,0.85)', cardinal:'#f0d990',
+ constellationLine:'rgba(132,152,214,0.58)', constellationLabel:'rgba(120,136,186,0.68)',
+ star: a=>`rgba(220,230,255,${a.toFixed(2)})`,
+ starLabel:'rgba(180,200,255,0.70)', shadowLine:'rgba(140,140,150,0.5)',
+ shadowMarker:'rgba(155,165,190,0.95)', shadowMarkerStroke:'rgba(15,20,32,0.95)',
+ shadowLabel:'#d7dcec'
+ };
+ }
+
+ function estimateTextWidth(text, fontSize, weight = 400) {
+ ctx.save();
+ ctx.font = `${weight} ${fontSize}px Segoe UI`;
+ const width = ctx.measureText(String(text)).width;
+ ctx.restore();
+ return width;
+ }
+
+ function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ function toSvgNumber(value) {
+ return Number(value).toFixed(2);
+ }
+
+ function buildSvgEndpointLabel(point, text, color, cx, cy, radius) {
+ const offsetX = point.x < cx ? 10 : -10;
+ const offsetY = point.y < cy ? -10 : 16;
+ const fontSize = 12;
+ const paddingX = 7;
+ const paddingY = 4;
+ const textWidth = estimateTextWidth(text, fontSize, 700);
+ let labelX = point.x + offsetX;
+ let labelY = point.y + offsetY;
+ if (offsetX < 0) labelX -= textWidth;
+ const minX = cx - radius + 6;
+ const maxX = cx + radius - textWidth - paddingX * 2 - 6;
+ const minY = cy - radius + fontSize + paddingY + 6;
+ const maxY = cy + radius - fontSize - paddingY - 6;
+ labelX = Math.max(minX, Math.min(maxX, labelX));
+ labelY = Math.max(minY, Math.min(maxY, labelY));
+ const boxX = labelX - paddingX;
+ const boxY = labelY - (fontSize / 2) - paddingY;
+ const boxW = textWidth + paddingX * 2;
+ const boxH = fontSize + paddingY * 2;
+ const fill = invertSky ? '#ffffff' : '#050814';
+ const stroke = invertSky ? '#1b2230' : 'rgba(255,255,255,0.22)';
+ return `
+
+
${escapeXml(text)}`;
+ }
+
function drawEndpointLabel(point, text, color, cx, cy, radius) {
const offsetX = point.x < cx ? 10 : -10;
const offsetY = point.y < cy ? -10 : 16;
@@ -927,8 +1023,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
function drawSky(passes, referenceTime=null, showStars=true) {
lastReferenceTime = referenceTime;
lastShowStars = showStars;
- const box = canvas.parentElement.getBoundingClientRect();
- const size = Math.max(320, Math.floor(Math.min(box.width, 820)));
+ const { size, cx, cy, radius } = getSkyRenderMetrics();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = size * dpr;
canvas.height = size * dpr;
@@ -936,25 +1031,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
canvas.style.height = `${size}px`;
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.clearRect(0,0,size,size);
- const cx=size/2, cy=size/2, radius=size*0.43;
-
- const palette = invertSky ? {
- skyFill: exportOpaqueSky ? 'rgb(252,252,248)' : 'rgba(252,252,248,0.98)',
- grid:'rgba(60,64,76,0.26)', gridText:'rgba(40,44,56,0.90)', cardinal:'#171b24',
- constellationLine:'rgba(88,96,118,0.76)', constellationLabel:'rgba(44,50,66,0.92)',
- star: a=>`rgba(10,12,18,${Math.max(0.72,a).toFixed(2)})`,
- starLabel:'rgba(18,22,32,0.94)', shadowLine:'rgba(150,154,166,0.82)',
- shadowMarker:'rgba(118,124,138,0.95)', shadowMarkerStroke:'rgba(24,28,36,0.95)',
- shadowLabel:'#111722'
- } : {
- skyFill: exportOpaqueSky ? 'rgb(4,6,20)' : 'rgba(4,6,20,0.55)',
- grid:'rgba(201,168,76,0.18)', gridText:'rgba(136,153,187,0.85)', cardinal:'#f0d990',
- constellationLine:'rgba(132,152,214,0.58)', constellationLabel:'rgba(120,136,186,0.68)',
- star: a=>`rgba(220,230,255,${a.toFixed(2)})`,
- starLabel:'rgba(180,200,255,0.70)', shadowLine:'rgba(140,140,150,0.5)',
- shadowMarker:'rgba(155,165,190,0.95)', shadowMarkerStroke:'rgba(15,20,32,0.95)',
- shadowLabel:'#d7dcec'
- };
+ const palette = getSkyPalette();
ctx.fillStyle = palette.skyFill;
ctx.beginPath(); ctx.arc(cx,cy,radius,0,Math.PI*2); ctx.fill();
@@ -973,7 +1050,8 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
ctx.fillStyle = palette.cardinal; ctx.font = '700 14px Segoe UI'; ctx.textAlign = 'center';
ctx.fillText('N', cx, cy-radius+18); ctx.fillText('S', cx, cy+radius-10);
- ctx.fillText('W', cx-radius+14, cy+5); ctx.fillText('O', cx+radius-14, cy+5);
+ ctx.fillText(mirrorEastWest ? 'O' : 'W', cx-radius+14, cy+5);
+ ctx.fillText(mirrorEastWest ? 'W' : 'O', cx+radius-14, cy+5);
ctx.fillText('Zenit', cx, cy-8);
const lat = Number(SITE_LOCATION.latitude), lon = Number(SITE_LOCATION.longitude);
@@ -1178,11 +1256,143 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
});
}
- function createSatpassExportFileName() {
+ function createSatpassExportFileName(extension = 'png') {
const date = lastSelectedPass?.aosTime instanceof Date ? lastSelectedPass.aosTime : new Date();
const parts = getTzParts(date, SITE_LOCATION.timezone);
const name = (lastSelectedSatelliteName || 'satellit').toLowerCase().replace(/[^a-z0-9]+/gi,'_').replace(/^_+|_+$/g,'') || 'satellit';
- return `satellitenhimmel_${name}_${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}_${padNumber(parts.hour)}-${padNumber(parts.minute)}.png`;
+ return `satellitenhimmel_${name}_${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}_${padNumber(parts.hour)}-${padNumber(parts.minute)}.${extension}`;
+ }
+
+ function generateSatpassSvgMarkup() {
+ const prevExport = exportOpaqueSky;
+ exportOpaqueSky = true;
+ try {
+ const { size, cx, cy, radius } = getSkyRenderMetrics();
+ const palette = getSkyPalette();
+ const lat = Number(SITE_LOCATION.latitude);
+ const lon = Number(SITE_LOCATION.longitude);
+ const starTime = lastReferenceTime ?? new Date();
+ const parts = [];
+ parts.push(``);
+ parts.push(`
`);
+ return parts.join('');
+ } finally {
+ exportOpaqueSky = prevExport;
+ }
+ }
+
+ async function exportSatpassSvg() {
+ try {
+ const markup = generateSatpassSvgMarkup();
+ const blob = new Blob([markup], { type: 'image/svg+xml;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = createSatpassExportFileName('svg');
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 2000);
+ } catch (err) {
+ console.error(err);
+ statusEl.textContent = 'SVG-Export fehlgeschlagen.';
+ }
}
async function exportSatpassPng() {
@@ -1193,7 +1403,8 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
const ec = document.createElement('canvas');
ec.width = canvas.width; ec.height = canvas.height;
const ectx = ec.getContext('2d');
- ectx.fillStyle = '#ffffff'; ectx.fillRect(0,0,ec.width,ec.height);
+ ectx.fillStyle = EXPORT_BACKGROUND_COLOR;
+ ectx.fillRect(0,0,ec.width,ec.height);
ectx.drawImage(canvas, 0, 0);
try {
await ensureExportLogoLoaded();
@@ -1204,7 +1415,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
const blob = await canvasToBlob(ec, 'image/png');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
- a.href = url; a.download = createSatpassExportFileName();
+ a.href = url; a.download = createSatpassExportFileName('png');
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 2000);
} catch (err) {
@@ -1224,10 +1435,19 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
invertBtn.classList.toggle('is-active', invertSky);
drawSky(lastSkyData, lastReferenceTime, lastShowStars);
});
+ mirrorBtn?.addEventListener('click', () => {
+ mirrorEastWest = !mirrorEastWest;
+ updateMirrorButton();
+ drawSky(lastSkyData, lastReferenceTime, lastShowStars);
+ });
exportBtn?.addEventListener('click', () => exportSatpassPng().catch(err => {
console.error(err); statusEl.textContent = 'PNG-Export fehlgeschlagen.';
}));
+ exportSvgBtn?.addEventListener('click', () => exportSatpassSvg().catch(err => {
+ console.error(err); statusEl.textContent = 'SVG-Export fehlgeschlagen.';
+ }));
+ updateMirrorButton();
buildDropdowns();
drawSky([], null, false);