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-panel">
|
||||||
<div class="satpass-map-controls">
|
<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="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="satpassInvertBtn">Invertiert</button>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" id="satpassMirrorBtn">Ost links / West rechts</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="satpassSkyTime" class="satpass-sky-time"></p>
|
<p id="satpassSkyTime" class="satpass-sky-time"></p>
|
||||||
<div class="satpass-canvas-wrap">
|
<div class="satpass-canvas-wrap">
|
||||||
@@ -505,7 +507,9 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
const statusEl = document.getElementById('satpassStatus');
|
const statusEl = document.getElementById('satpassStatus');
|
||||||
const canvas = document.getElementById('satSkyCanvas');
|
const canvas = document.getElementById('satSkyCanvas');
|
||||||
const exportBtn = document.getElementById('satpassExportBtn');
|
const exportBtn = document.getElementById('satpassExportBtn');
|
||||||
|
const exportSvgBtn = document.getElementById('satpassExportSvgBtn');
|
||||||
const invertBtn = document.getElementById('satpassInvertBtn');
|
const invertBtn = document.getElementById('satpassInvertBtn');
|
||||||
|
const mirrorBtn = document.getElementById('satpassMirrorBtn');
|
||||||
const skyTimeEl = document.getElementById('satpassSkyTime');
|
const skyTimeEl = document.getElementById('satpassSkyTime');
|
||||||
const satSelectEl = document.getElementById('satSelect');
|
const satSelectEl = document.getElementById('satSelect');
|
||||||
const passSelectEl = document.getElementById('passSelect');
|
const passSelectEl = document.getElementById('passSelect');
|
||||||
@@ -513,7 +517,9 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const exportLogoImg = new Image();
|
const exportLogoImg = new Image();
|
||||||
exportLogoImg.src = 'images/logoAM_sw.png';
|
exportLogoImg.src = 'images/logoAM_sw.png';
|
||||||
|
const EXPORT_BACKGROUND_COLOR = '#f3efe6';
|
||||||
let invertSky = false;
|
let invertSky = false;
|
||||||
|
let mirrorEastWest = true;
|
||||||
let exportOpaqueSky = false;
|
let exportOpaqueSky = false;
|
||||||
let lastReferenceTime = null;
|
let lastReferenceTime = null;
|
||||||
let lastShowStars = false;
|
let lastShowStars = false;
|
||||||
@@ -521,6 +527,20 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
let lastSelectedPass = null;
|
let lastSelectedPass = null;
|
||||||
let lastSkyData = [];
|
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 deg2rad(deg) { return deg * Math.PI / 180; }
|
||||||
function rad2deg(rad) { return rad * 180 / Math.PI; }
|
function rad2deg(rad) { return rad * 180 / Math.PI; }
|
||||||
|
|
||||||
@@ -594,13 +614,15 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function azToCardinal(az) {
|
function azToCardinal(az) {
|
||||||
const dirs = ['N','NO','O','SO','S','SW','W','NW'];
|
const displayAz = getDisplayedAzimuthDeg(az);
|
||||||
return dirs[Math.round(az/45) % 8];
|
const dirs = ['N','NW','W','SW','S','SO','O','NO'];
|
||||||
|
return dirs[Math.round(displayAz/45) % 8];
|
||||||
}
|
}
|
||||||
|
|
||||||
function azToDirectionName(az) {
|
function azToDirectionName(az) {
|
||||||
const dirs = ['Norden','Nordosten','Osten','Suedosten','Sueden','Suedwesten','Westen','Nordwesten'];
|
const displayAz = getDisplayedAzimuthDeg(az);
|
||||||
return dirs[Math.round(az/45) % 8];
|
const dirs = ['Norden','Nordwesten','Westen','Suedwesten','Sueden','Suedosten','Osten','Nordosten'];
|
||||||
|
return dirs[Math.round(displayAz/45) % 8];
|
||||||
}
|
}
|
||||||
|
|
||||||
function altitudeToPhrase(el) {
|
function altitudeToPhrase(el) {
|
||||||
@@ -857,10 +879,84 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
|
|
||||||
function projectPoint(azimuthDeg, elevationDeg, radius, cx, cy) {
|
function projectPoint(azimuthDeg, elevationDeg, radius, cx, cy) {
|
||||||
const r = ((90 - elevationDeg) / 90) * radius;
|
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) };
|
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) {
|
function drawEndpointLabel(point, text, color, cx, cy, radius) {
|
||||||
const offsetX = point.x < cx ? 10 : -10;
|
const offsetX = point.x < cx ? 10 : -10;
|
||||||
const offsetY = point.y < cy ? -10 : 16;
|
const offsetY = point.y < cy ? -10 : 16;
|
||||||
@@ -927,8 +1023,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
function drawSky(passes, referenceTime=null, showStars=true) {
|
function drawSky(passes, referenceTime=null, showStars=true) {
|
||||||
lastReferenceTime = referenceTime;
|
lastReferenceTime = referenceTime;
|
||||||
lastShowStars = showStars;
|
lastShowStars = showStars;
|
||||||
const box = canvas.parentElement.getBoundingClientRect();
|
const { size, cx, cy, radius } = getSkyRenderMetrics();
|
||||||
const size = Math.max(320, Math.floor(Math.min(box.width, 820)));
|
|
||||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
canvas.width = size * dpr;
|
canvas.width = size * dpr;
|
||||||
canvas.height = size * dpr;
|
canvas.height = size * dpr;
|
||||||
@@ -936,25 +1031,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
canvas.style.height = `${size}px`;
|
canvas.style.height = `${size}px`;
|
||||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||||
ctx.clearRect(0,0,size,size);
|
ctx.clearRect(0,0,size,size);
|
||||||
const cx=size/2, cy=size/2, radius=size*0.43;
|
const palette = getSkyPalette();
|
||||||
|
|
||||||
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'
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.fillStyle = palette.skyFill;
|
ctx.fillStyle = palette.skyFill;
|
||||||
ctx.beginPath(); ctx.arc(cx,cy,radius,0,Math.PI*2); ctx.fill();
|
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.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('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);
|
ctx.fillText('Zenit', cx, cy-8);
|
||||||
|
|
||||||
const lat = Number(SITE_LOCATION.latitude), lon = Number(SITE_LOCATION.longitude);
|
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 date = lastSelectedPass?.aosTime instanceof Date ? lastSelectedPass.aosTime : new Date();
|
||||||
const parts = getTzParts(date, SITE_LOCATION.timezone);
|
const parts = getTzParts(date, SITE_LOCATION.timezone);
|
||||||
const name = (lastSelectedSatelliteName || 'satellit').toLowerCase().replace(/[^a-z0-9]+/gi,'_').replace(/^_+|_+$/g,'') || 'satellit';
|
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() {
|
async function exportSatpassPng() {
|
||||||
@@ -1193,7 +1403,8 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
const ec = document.createElement('canvas');
|
const ec = document.createElement('canvas');
|
||||||
ec.width = canvas.width; ec.height = canvas.height;
|
ec.width = canvas.width; ec.height = canvas.height;
|
||||||
const ectx = ec.getContext('2d');
|
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);
|
ectx.drawImage(canvas, 0, 0);
|
||||||
try {
|
try {
|
||||||
await ensureExportLogoLoaded();
|
await ensureExportLogoLoaded();
|
||||||
@@ -1204,7 +1415,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
const blob = await canvasToBlob(ec, 'image/png');
|
const blob = await canvasToBlob(ec, 'image/png');
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
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();
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1224,10 +1435,19 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
|||||||
invertBtn.classList.toggle('is-active', invertSky);
|
invertBtn.classList.toggle('is-active', invertSky);
|
||||||
drawSky(lastSkyData, lastReferenceTime, lastShowStars);
|
drawSky(lastSkyData, lastReferenceTime, lastShowStars);
|
||||||
});
|
});
|
||||||
|
mirrorBtn?.addEventListener('click', () => {
|
||||||
|
mirrorEastWest = !mirrorEastWest;
|
||||||
|
updateMirrorButton();
|
||||||
|
drawSky(lastSkyData, lastReferenceTime, lastShowStars);
|
||||||
|
});
|
||||||
exportBtn?.addEventListener('click', () => exportSatpassPng().catch(err => {
|
exportBtn?.addEventListener('click', () => exportSatpassPng().catch(err => {
|
||||||
console.error(err); statusEl.textContent = 'PNG-Export fehlgeschlagen.';
|
console.error(err); statusEl.textContent = 'PNG-Export fehlgeschlagen.';
|
||||||
}));
|
}));
|
||||||
|
exportSvgBtn?.addEventListener('click', () => exportSatpassSvg().catch(err => {
|
||||||
|
console.error(err); statusEl.textContent = 'SVG-Export fehlgeschlagen.';
|
||||||
|
}));
|
||||||
|
|
||||||
|
updateMirrorButton();
|
||||||
buildDropdowns();
|
buildDropdowns();
|
||||||
drawSky([], null, false);
|
drawSky([], null, false);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user