Satellitenhimmel verbessert
This commit is contained in:
+251
-31
@@ -451,7 +451,9 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
||||
<div class="satpass-map-panel">
|
||||
<div class="satpass-map-controls">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="satpassExportBtn">PNG Export</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="satpassExportSvgBtn">SVG Export</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="satpassInvertBtn">Invertiert</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="satpassMirrorBtn">Ost links / West rechts</button>
|
||||
</div>
|
||||
<p id="satpassSkyTime" class="satpass-sky-time"></p>
|
||||
<div class="satpass-canvas-wrap">
|
||||
@@ -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, '"')
|
||||
.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 `
|
||||
<rect x="${toSvgNumber(boxX)}" y="${toSvgNumber(boxY)}" width="${toSvgNumber(boxW)}" height="${toSvgNumber(boxH)}" fill="${fill}" stroke="${stroke}" stroke-width="1.2" />
|
||||
<text x="${toSvgNumber(labelX)}" y="${toSvgNumber(labelY)}" fill="${color}" font-family="Segoe UI, Arial, sans-serif" font-size="${fontSize}" font-weight="700" dominant-baseline="middle">${escapeXml(text)}</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(`<?xml version="1.0" encoding="UTF-8"?>`);
|
||||
parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" role="img" aria-label="Satellitenhimmel">`);
|
||||
parts.push(`<rect width="${size}" height="${size}" fill="#ffffff" />`);
|
||||
parts.push(`<circle cx="${toSvgNumber(cx)}" cy="${toSvgNumber(cy)}" r="${toSvgNumber(radius)}" fill="${palette.skyFill}" />`);
|
||||
|
||||
[radius, radius*(2/3), radius*(1/3)].forEach((r, i) => {
|
||||
parts.push(`<circle cx="${toSvgNumber(cx)}" cy="${toSvgNumber(cy)}" r="${toSvgNumber(r)}" fill="none" stroke="${palette.grid}" stroke-width="1" />`);
|
||||
parts.push(`<text x="${toSvgNumber(cx + 6)}" y="${toSvgNumber(cy - r + 14)}" fill="${palette.gridText}" font-family="Segoe UI, Arial, sans-serif" font-size="12">${i===0?0:i===1?30:60}°</text>`);
|
||||
});
|
||||
parts.push(`<line x1="${toSvgNumber(cx)}" y1="${toSvgNumber(cy - radius)}" x2="${toSvgNumber(cx)}" y2="${toSvgNumber(cy + radius)}" stroke="${palette.grid}" stroke-width="1" />`);
|
||||
parts.push(`<line x1="${toSvgNumber(cx - radius)}" y1="${toSvgNumber(cy)}" x2="${toSvgNumber(cx + radius)}" y2="${toSvgNumber(cy)}" stroke="${palette.grid}" stroke-width="1" />`);
|
||||
parts.push(`<text x="${toSvgNumber(cx)}" y="${toSvgNumber(cy - radius + 18)}" fill="${palette.cardinal}" font-family="Segoe UI, Arial, sans-serif" font-size="14" font-weight="700" text-anchor="middle">N</text>`);
|
||||
parts.push(`<text x="${toSvgNumber(cx)}" y="${toSvgNumber(cy + radius - 10)}" fill="${palette.cardinal}" font-family="Segoe UI, Arial, sans-serif" font-size="14" font-weight="700" text-anchor="middle">S</text>`);
|
||||
parts.push(`<text x="${toSvgNumber(cx - radius + 14)}" y="${toSvgNumber(cy + 5)}" fill="${palette.cardinal}" font-family="Segoe UI, Arial, sans-serif" font-size="14" font-weight="700" text-anchor="middle">${mirrorEastWest ? 'O' : 'W'}</text>`);
|
||||
parts.push(`<text x="${toSvgNumber(cx + radius - 14)}" y="${toSvgNumber(cy + 5)}" fill="${palette.cardinal}" font-family="Segoe UI, Arial, sans-serif" font-size="14" font-weight="700" text-anchor="middle">${mirrorEastWest ? 'W' : 'O'}</text>`);
|
||||
parts.push(`<text x="${toSvgNumber(cx)}" y="${toSvgNumber(cy - 8)}" fill="${palette.cardinal}" font-family="Segoe UI, Arial, sans-serif" font-size="14" font-weight="700" text-anchor="middle">Zenit</text>`);
|
||||
|
||||
for (const line of CONSTELLATION_LINES) {
|
||||
if (!Array.isArray(line) || line.length < 2) continue;
|
||||
let segment = [];
|
||||
for (const point of line) {
|
||||
const pos = equatorialToHorizontal(point.ra, point.dec, lat, lon, starTime);
|
||||
if (pos.altDeg < 0) {
|
||||
if (segment.length > 1) {
|
||||
parts.push(`<polyline points="${segment.map(p => `${toSvgNumber(p.x)},${toSvgNumber(p.y)}`).join(' ')}" fill="none" stroke="${palette.constellationLine}" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round" />`);
|
||||
}
|
||||
segment = [];
|
||||
continue;
|
||||
}
|
||||
segment.push(projectPoint(pos.azimuthDeg, pos.altDeg, radius, cx, cy));
|
||||
}
|
||||
if (segment.length > 1) {
|
||||
parts.push(`<polyline points="${segment.map(p => `${toSvgNumber(p.x)},${toSvgNumber(p.y)}`).join(' ')}" fill="none" stroke="${palette.constellationLine}" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round" />`);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastShowStars) {
|
||||
const occupiedLabelRects = [];
|
||||
for (const star of BRIGHT_STARS) {
|
||||
const pos = equatorialToHorizontal(star.ra, star.dec, lat, lon, starTime);
|
||||
if (pos.altDeg < 0) continue;
|
||||
const pt = projectPoint(pos.azimuthDeg, pos.altDeg, radius, cx, cy);
|
||||
const starRadius = Math.max(1.0, Math.min(5.0, 4.5 - star.mag*1.0));
|
||||
const alpha = Math.max(0.5, Math.min(0.95, 0.95 - star.mag*0.12));
|
||||
parts.push(`<circle cx="${toSvgNumber(pt.x)}" cy="${toSvgNumber(pt.y)}" r="${toSvgNumber(starRadius)}" fill="${palette.star(alpha)}" />`);
|
||||
if (star.name && star.mag <= 2.0) {
|
||||
const lx = pt.x + starRadius + 3;
|
||||
const ly = pt.y + 3;
|
||||
const lw = estimateTextWidth(star.name, 10);
|
||||
parts.push(`<text x="${toSvgNumber(lx)}" y="${toSvgNumber(ly)}" fill="${palette.starLabel}" font-family="Segoe UI, Arial, sans-serif" font-size="10">${escapeXml(star.name)}</text>`);
|
||||
occupiedLabelRects.push(buildTextRect(lx, ly, lw, 10));
|
||||
}
|
||||
}
|
||||
for (const con of CONSTELLATION_LABELS) {
|
||||
const pos = equatorialToHorizontal(con.ra, con.dec, lat, lon, starTime);
|
||||
if (pos.altDeg < 0) continue;
|
||||
const pt = projectPoint(pos.azimuthDeg, pos.altDeg, radius, cx, cy);
|
||||
const lw = estimateTextWidth(con.label, 11);
|
||||
const pl = findFreeLabelPlacement(pt.x, pt.y, lw, 11, occupiedLabelRects, cx, cy, radius, 'center');
|
||||
if (!pl) continue;
|
||||
parts.push(`<text x="${toSvgNumber(pl.x)}" y="${toSvgNumber(pl.y)}" fill="${palette.constellationLabel}" font-family="Segoe UI, Arial, sans-serif" font-size="11">${escapeXml(con.label)}</text>`);
|
||||
occupiedLabelRects.push(pl.rect);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of lastSkyData.filter(entry => entry?.pass)) {
|
||||
const pass = item.pass;
|
||||
const pts = pass.path.filter(p => p.elevationDeg >= 0);
|
||||
if (pts.length < 2) continue;
|
||||
const trackColor = invertSky ? 'rgba(8,10,14,0.96)' : (PYTHON_RESULTS[0]?.color ?? '#f0d990');
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p0 = projectPoint(pts[i].azimuthDeg, pts[i].elevationDeg, radius, cx, cy);
|
||||
const p1 = projectPoint(pts[i + 1].azimuthDeg, pts[i + 1].elevationDeg, radius, cx, cy);
|
||||
if (pts[i].shadow) {
|
||||
parts.push(`<line x1="${toSvgNumber(p0.x)}" y1="${toSvgNumber(p0.y)}" x2="${toSvgNumber(p1.x)}" y2="${toSvgNumber(p1.y)}" stroke="${palette.shadowLine}" stroke-width="1" stroke-linecap="round" />`);
|
||||
} else {
|
||||
parts.push(`<line x1="${toSvgNumber(p0.x)}" y1="${toSvgNumber(p0.y)}" x2="${toSvgNumber(p1.x)}" y2="${toSvgNumber(p1.y)}" stroke="${trackColor}" stroke-width="${invertSky ? '3.2' : '2.5'}" stroke-linecap="round" />`);
|
||||
}
|
||||
}
|
||||
const startPt = projectPoint(pts[0].azimuthDeg, pts[0].elevationDeg, radius, cx, cy);
|
||||
const endPt = projectPoint(pts[pts.length - 1].azimuthDeg, pts[pts.length - 1].elevationDeg, radius, cx, cy);
|
||||
parts.push(`<circle cx="${toSvgNumber(startPt.x)}" cy="${toSvgNumber(startPt.y)}" r="3.5" fill="${trackColor}" />`);
|
||||
parts.push(`<circle cx="${toSvgNumber(endPt.x)}" cy="${toSvgNumber(endPt.y)}" r="3.5" fill="${trackColor}" />`);
|
||||
parts.push(buildSvgEndpointLabel(startPt, formatTime(pass.aosTime), trackColor, cx, cy, radius));
|
||||
parts.push(buildSvgEndpointLabel(endPt, formatTime(pass.losTime), trackColor, cx, cy, radius));
|
||||
if (isPeakVisible(pass)) {
|
||||
const peakPt = projectPoint(pass.peakAzimuth, pass.peakElevation, radius, cx, cy);
|
||||
parts.push(`<circle cx="${toSvgNumber(peakPt.x)}" cy="${toSvgNumber(peakPt.y)}" r="4.5" fill="${trackColor}" stroke="${invertSky ? 'rgba(248,248,244,0.95)' : 'rgba(8,12,20,0.92)'}" stroke-width="1.5" />`);
|
||||
parts.push(buildSvgEndpointLabel(peakPt, formatTime(pass.peakTime), trackColor, cx, cy, radius));
|
||||
}
|
||||
if (pass.shadowEntry) {
|
||||
const sp = projectPoint(pass.shadowEntry.azimuthDeg, pass.shadowEntry.elevationDeg, radius, cx, cy);
|
||||
parts.push(`<circle cx="${toSvgNumber(sp.x)}" cy="${toSvgNumber(sp.y)}" r="4" fill="${palette.shadowMarker}" stroke="${palette.shadowMarkerStroke}" stroke-width="1.5" />`);
|
||||
parts.push(buildSvgEndpointLabel(sp, `ES ${formatTime(pass.shadowEntry.time)}`, palette.shadowLabel, cx, cy, radius));
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(`</svg>`);
|
||||
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);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user